{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "JL3ZPJPgC4wC"
   },
   "source": [
    "# Shakespeare Text Generation (using RNN LSTM)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "eoWGtkL8PPJ3"
   },
   "source": [
    "> - 🤖 See [full list of Machine Learning Experiments](https://github.com/trekhleb/machine-learning-experiments) on **GitHub**<br/><br/>\n",
    "> - ▶️ **Interactive Demo**: [try this model and other machine learning experiments in action](https://trekhleb.github.io/machine-learning-experiments/)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "bui0MyTjv1Mp"
   },
   "source": [
    "## Experiment overview\n",
    "\n",
    "In this experiment we will use character-based [Recurrent Neural Network](https://en.wikipedia.org/wiki/Recurrent_neural_network) (RNN) to generate a Shakespeare's-like text based on the Shakespeare dataset from [The Unreasonable Effectiveness of Recurrent Neural Networks](http://karpathy.github.io/2015/05/21/rnn-effectiveness/) blog post.\n",
    "\n",
    "For this experiment we will use [Tensorflow v2](https://www.tensorflow.org/) with its [Keras API](https://www.tensorflow.org/guide/keras).\n",
    "\n",
    "![text_generation_shakespeare_rnn.jpg](../../demos/src/images/text_generation_shakespeare_rnn.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "3XxEZuRNIzHH"
   },
   "source": [
    "_Inspired by [Text generation with an RNN](https://www.tensorflow.org/tutorials/text/text_generation)_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "IDJctDhhEDTD"
   },
   "source": [
    "## Import dependencies"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 34
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1347,
     "status": "ok",
     "timestamp": 1584519798294,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "Z2xZnxncD2Ep",
    "outputId": "9e110fb9-a6d1-4bd6-818c-2198114e3133"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TensorFlow 2.x selected.\n"
     ]
    }
   ],
   "source": [
    "# Selecting Tensorflow version v2 (the command is relevant for Colab only).\n",
    "# %tensorflow_version 2.x"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 68
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 2811,
     "status": "ok",
     "timestamp": 1584519800384,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "SpueB6zADYgE",
    "outputId": "3f4e9997-8ce8-4ef8-f8f9-320c9a44e03b"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Python version: 3.6.9\n",
      "Tensorflow version: 2.1.0\n",
      "Keras version: 2.2.4-tf\n"
     ]
    }
   ],
   "source": [
    "import tensorflow as tf\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import platform\n",
    "import time\n",
    "import pathlib\n",
    "import os\n",
    "\n",
    "print('Python version:', platform.python_version())\n",
    "print('Tensorflow version:', tf.__version__)\n",
    "print('Keras version:', tf.keras.__version__)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "ciI_JnnNEGCw"
   },
   "source": [
    "## Download the dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 68
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 2533,
     "status": "ok",
     "timestamp": 1584519800396,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "Vk2HldshEOph",
    "outputId": "45342704-eb1c-4863-a4e4-8a94d9cc764b"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt\n",
      "1122304/1115394 [==============================] - 0s 0us/step\n",
      "/content/tmp/datasets/shakespeare.txt\n"
     ]
    }
   ],
   "source": [
    "cache_dir = './tmp'\n",
    "dataset_file_name = 'shakespeare.txt'\n",
    "dataset_file_origin = 'https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt'\n",
    "\n",
    "dataset_file_path = tf.keras.utils.get_file(\n",
    "    fname=dataset_file_name,\n",
    "    origin=dataset_file_origin,\n",
    "    cache_dir=pathlib.Path(cache_dir).absolute()\n",
    ")\n",
    "\n",
    "print(dataset_file_path)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "eKTy6YS5Gx-g"
   },
   "source": [
    "## Analyze the dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 34
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 2199,
     "status": "ok",
     "timestamp": 1584519800398,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "CzfX7hK8G0bn",
    "outputId": "dbcd9c2e-967d-404f-ebd4-387ee0b629d7"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Length of text: 1115394 characters\n"
     ]
    }
   ],
   "source": [
    "# Reading the database file.\n",
    "text = open(dataset_file_path, mode='r').read()\n",
    "\n",
    "print('Length of text: {} characters'.format(len(text)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 272
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1774,
     "status": "ok",
     "timestamp": 1584519800402,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "nknOAOuoH-Av",
    "outputId": "7030d3f8-c6a8-43e0-81a0-c1e87d677314"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "First Citizen:\n",
      "Before we proceed any further, hear me speak.\n",
      "\n",
      "All:\n",
      "Speak, speak.\n",
      "\n",
      "First Citizen:\n",
      "You are all resolved rather to die than to famish?\n",
      "\n",
      "All:\n",
      "Resolved. resolved.\n",
      "\n",
      "First Citizen:\n",
      "First, you know Caius Marcius is chief enemy to the people.\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Take a look at the first 250 characters in text.\n",
    "print(text[:250])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 71
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1590,
     "status": "ok",
     "timestamp": 1584519800403,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "VPXkB5bOITqi",
    "outputId": "69fdb374-4b35-442d-a934-7017245ea18e"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "65 unique characters\n",
      "vocab: ['\\n', ' ', '!', '$', '&', \"'\", ',', '-', '.', '3', ':', ';', '?', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']\n"
     ]
    }
   ],
   "source": [
    "# The unique characters in the file\n",
    "vocab = sorted(set(text))\n",
    "\n",
    "print('{} unique characters'.format(len(vocab)))\n",
    "print('vocab:', vocab)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "GqpuKh9HMNnf"
   },
   "source": [
    "## Process the dataset"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "6dj4e-AGMaV4"
   },
   "source": [
    "### Vectorize the text\n",
    "\n",
    "Before feeding the text to our RNN we need to convert the text from a sequence of characters to a sequence of numbers. To do so we will detect all unique characters in the text, form a vocabulary out of it and replace each character with its index in the vocabulary."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 408
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1510,
     "status": "ok",
     "timestamp": 1584519800417,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "xFFpuXfGMPq2",
    "outputId": "530395b0-62e2-41e5-c0bf-63b2ca9da7cf"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{\n",
      "  '\\n':   0,\n",
      "  ' ' :   1,\n",
      "  '!' :   2,\n",
      "  '$' :   3,\n",
      "  '&' :   4,\n",
      "  \"'\" :   5,\n",
      "  ',' :   6,\n",
      "  '-' :   7,\n",
      "  '.' :   8,\n",
      "  '3' :   9,\n",
      "  ':' :  10,\n",
      "  ';' :  11,\n",
      "  '?' :  12,\n",
      "  'A' :  13,\n",
      "  'B' :  14,\n",
      "  'C' :  15,\n",
      "  'D' :  16,\n",
      "  'E' :  17,\n",
      "  'F' :  18,\n",
      "  'G' :  19,\n",
      "  ...\n",
      "}\n"
     ]
    }
   ],
   "source": [
    "# Map characters to their indices in vocabulary.\n",
    "char2index = {char: index for index, char in enumerate(vocab)}\n",
    "\n",
    "print('{')\n",
    "for char, _ in zip(char2index, range(20)):\n",
    "    print('  {:4s}: {:3d},'.format(repr(char), char2index[char]))\n",
    "print('  ...\\n}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 85
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1466,
     "status": "ok",
     "timestamp": 1584519800417,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "lQB33zI7NkRo",
    "outputId": "ef5bc93f-7bad-437f-f70a-9eb53d895613"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['\\n' ' ' '!' '$' '&' \"'\" ',' '-' '.' '3' ':' ';' '?' 'A' 'B' 'C' 'D' 'E'\n",
      " 'F' 'G' 'H' 'I' 'J' 'K' 'L' 'M' 'N' 'O' 'P' 'Q' 'R' 'S' 'T' 'U' 'V' 'W'\n",
      " 'X' 'Y' 'Z' 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o'\n",
      " 'p' 'q' 'r' 's' 't' 'u' 'v' 'w' 'x' 'y' 'z']\n"
     ]
    }
   ],
   "source": [
    "# Map character indices to characters from vacabulary.\n",
    "index2char = np.array(vocab)\n",
    "print(index2char)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 51
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1367,
     "status": "ok",
     "timestamp": 1584519800418,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "DXUAlYmvN_Rj",
    "outputId": "ef3412c4-68e0-4362-e093-feba628b7079"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "text_as_int length: 1115394\n",
      "'First Citizen:\\n' --> array([18, 47, 56, 57, 58,  1, 15, 47, 58, 47, 64, 43, 52, 10,  0])\n"
     ]
    }
   ],
   "source": [
    "# Convert chars in text to indices.\n",
    "text_as_int = np.array([char2index[char] for char in text])\n",
    "\n",
    "print('text_as_int length: {}'.format(len(text_as_int)))\n",
    "print('{} --> {}'.format(repr(text[:15]), repr(text_as_int[:15])))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "CHv5HhUuTQYS"
   },
   "source": [
    "## Create training sequences"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 34
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1227,
     "status": "ok",
     "timestamp": 1584519800418,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "9tHT8IXGTTtZ",
    "outputId": "489b7e07-e898-49bc-e099-a9e7c4791c8c"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "examples_per_epoch: 11043\n"
     ]
    }
   ],
   "source": [
    "# The maximum length sentence we want for a single input in characters.\n",
    "sequence_length = 100\n",
    "examples_per_epoch = len(text) // (sequence_length + 1)\n",
    "\n",
    "print('examples_per_epoch:', examples_per_epoch)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 102
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1331,
     "status": "ok",
     "timestamp": 1584519800779,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "Pf5tMMDnUP3q",
    "outputId": "f8e191f6-df0b-426b-9905-af62be4d605c"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "F\n",
      "i\n",
      "r\n",
      "s\n",
      "t\n"
     ]
    }
   ],
   "source": [
    "# Create training dataset.\n",
    "char_dataset = tf.data.Dataset.from_tensor_slices(text_as_int)\n",
    "\n",
    "for char in char_dataset.take(5):\n",
    "    print(index2char[char.numpy()])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 156
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 3082,
     "status": "ok",
     "timestamp": 1584519802697,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "Ap71VjB2Vuct",
    "outputId": "0c05de4a-d66c-4f20-83a7-e584d853e98d"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Sequences count: 11043\n",
      "\n",
      "'First Citizen:\\nBefore we proceed any further, hear me speak.\\n\\nAll:\\nSpeak, speak.\\n\\nFirst Citizen:\\nYou '\n",
      "'are all resolved rather to die than to famish?\\n\\nAll:\\nResolved. resolved.\\n\\nFirst Citizen:\\nFirst, you k'\n",
      "\"now Caius Marcius is chief enemy to the people.\\n\\nAll:\\nWe know't, we know't.\\n\\nFirst Citizen:\\nLet us ki\"\n",
      "\"ll him, and we'll have corn at our own price.\\nIs't a verdict?\\n\\nAll:\\nNo more talking on't; let it be d\"\n",
      "'one: away, away!\\n\\nSecond Citizen:\\nOne word, good citizens.\\n\\nFirst Citizen:\\nWe are accounted poor citi'\n"
     ]
    }
   ],
   "source": [
    "# Generate batched sequences out of the char_dataset.\n",
    "sequences = char_dataset.batch(sequence_length + 1, drop_remainder=True)\n",
    "\n",
    "# Sequences size is the same as examples_per_epoch.\n",
    "print('Sequences count: {}'.format(len(list(sequences.as_numpy_iterator()))));\n",
    "print()\n",
    "\n",
    "# Sequences examples.\n",
    "for item in sequences.take(5):\n",
    "    print(repr(''.join(index2char[item.numpy()])))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "Y8spPCfe-iTn"
   },
   "outputs": [],
   "source": [
    "# sequences shape:\n",
    "# - 11043 sequences\n",
    "# - Each sequence of length 101\n",
    "#\n",
    "#\n",
    "#    101     101          101\n",
    "# [(.....) (.....) ...  (.....)]\n",
    "#\n",
    "# <---------- 11043 ----------->"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "HdcrcUs4Xxso"
   },
   "source": [
    "For each sequence, duplicate and shift it to form the input and target text. For example, say `sequence_length` is `4` and our text is `Hello`. The input sequence would be `Hell`, and the target sequence `ello`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "9fxvXsP0XFDh"
   },
   "outputs": [],
   "source": [
    "def split_input_target(chunk):\n",
    "    input_text = chunk[:-1]\n",
    "    target_text = chunk[1:]\n",
    "    return input_text, target_text"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 34
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 6915,
     "status": "ok",
     "timestamp": 1584519807164,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "454rWIQYXXRY",
    "outputId": "de3a70e1-49e8-4f68-d988-77d1856a2e60"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "dataset size: 11043\n"
     ]
    }
   ],
   "source": [
    "dataset = sequences.map(split_input_target)\n",
    "\n",
    "# Dataset size is the same as examples_per_epoch.\n",
    "# But each element of a sequence is now has length of `sequence_length`\n",
    "# and not `sequence_length + 1`.\n",
    "print('dataset size: {}'.format(len(list(dataset.as_numpy_iterator()))))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 122
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 6548,
     "status": "ok",
     "timestamp": 1584519807165,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "Kuoh4tCdYCck",
    "outputId": "b03368eb-4c92-4662-95fe-d64f6bec308d"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Input sequence size: 100\n",
      "Target sequence size: 100\n",
      "\n",
      "Input: 'First Citizen:\\nBefore we proceed any further, hear me speak.\\n\\nAll:\\nSpeak, speak.\\n\\nFirst Citizen:\\nYou'\n",
      "Target: 'irst Citizen:\\nBefore we proceed any further, hear me speak.\\n\\nAll:\\nSpeak, speak.\\n\\nFirst Citizen:\\nYou '\n"
     ]
    }
   ],
   "source": [
    "for input_example, target_example in dataset.take(1):\n",
    "    print('Input sequence size:', repr(len(input_example.numpy())))\n",
    "    print('Target sequence size:', repr(len(target_example.numpy())))\n",
    "    print()\n",
    "    print('Input:', repr(''.join(index2char[input_example.numpy()])))\n",
    "    print('Target:', repr(''.join(index2char[target_example.numpy()])))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "cp0tl0sN807l"
   },
   "outputs": [],
   "source": [
    "# dataset shape:\n",
    "# - 11043 sequences\n",
    "# - Each sequence is a tuple of 2 sub-sequences of length 100 (input_text and target_text)\n",
    "#\n",
    "#\n",
    "#    100       100           100\n",
    "# /(.....)\\ /(.....)\\ ... /(.....)\\  <-- input_text\n",
    "# \\(.....)/ \\(.....)/     \\(.....)/  <-- target_text\n",
    "#\n",
    "# <----------- 11043 ------------->"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "BDYHEJ0pY1ai"
   },
   "source": [
    "Each index of these vectors are processed as one time step. For the input at time step 0, the model receives the index for \"F\" and trys to predict the index for \"i\" as the next character. At the next timestep, it does the same thing but the RNN considers the previous step context in addition to the current input character."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 272
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 6469,
     "status": "ok",
     "timestamp": 1584519807584,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "C-0zpv53Y2o4",
    "outputId": "05fb1543-308c-42f9-d2ee-003c2454772b"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Step  0\n",
      "  input: 18 ('F')\n",
      "  expected output: 47 ('i')\n",
      "Step  1\n",
      "  input: 47 ('i')\n",
      "  expected output: 56 ('r')\n",
      "Step  2\n",
      "  input: 56 ('r')\n",
      "  expected output: 57 ('s')\n",
      "Step  3\n",
      "  input: 57 ('s')\n",
      "  expected output: 58 ('t')\n",
      "Step  4\n",
      "  input: 58 ('t')\n",
      "  expected output: 1 (' ')\n"
     ]
    }
   ],
   "source": [
    "for i, (input_idx, target_idx) in enumerate(zip(input_example[:5], target_example[:5])):\n",
    "    print('Step {:2d}'.format(i))\n",
    "    print('  input: {} ({:s})'.format(input_idx, repr(index2char[input_idx])))\n",
    "    print('  expected output: {} ({:s})'.format(target_idx, repr(index2char[target_idx])))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "1iDlp40lC5YB"
   },
   "source": [
    "## Split training sequences into batches\n",
    "\n",
    "We used `tf.data` to split the text into manageable sequences. But before feeding this data into the model, we need to shuffle the data and pack it into batches."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 34
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 6256,
     "status": "ok",
     "timestamp": 1584519807585,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "eDq-wa5EC3wW",
    "outputId": "d13224e8-062a-4df8-c232-04f60d572e8d"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<BatchDataset shapes: ((64, 100), (64, 100)), types: (tf.int64, tf.int64)>"
      ]
     },
     "execution_count": 19,
     "metadata": {
      "tags": []
     },
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Batch size.\n",
    "BATCH_SIZE = 64\n",
    "\n",
    "# Buffer size to shuffle the dataset (TF data is designed to work\n",
    "# with possibly infinite sequences, so it doesn't attempt to shuffle\n",
    "# the entire sequence in memory. Instead, it maintains a buffer in\n",
    "# which it shuffles elements).\n",
    "BUFFER_SIZE = 10000\n",
    "\n",
    "dataset = dataset.shuffle(BUFFER_SIZE).batch(BATCH_SIZE, drop_remainder=True)\n",
    "\n",
    "dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 34
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 8229,
     "status": "ok",
     "timestamp": 1584519809742,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "1x4puZiiOlyl",
    "outputId": "1debcd12-46ba-4b5f-aa56-a162fbe76f1a"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Batched dataset size: 172\n"
     ]
    }
   ],
   "source": [
    "print('Batched dataset size: {}'.format(len(list(dataset.as_numpy_iterator()))))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 306
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 9749,
     "status": "ok",
     "timestamp": 1584519811500,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "b_kYgvQGBO0U",
    "outputId": "2dfda8dc-a501-4e5f-8835-6c87b8f77051"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1st batch: input_text: tf.Tensor(\n",
      "[[39 58 59 ... 57 58 53]\n",
      " [47 52  1 ...  1 63 53]\n",
      " [53 61  1 ... 46 57  6]\n",
      " ...\n",
      " [ 1 57 43 ... 52 42  8]\n",
      " [58 46 43 ... 43  1 58]\n",
      " [56 43 47 ... 53 59  1]], shape=(64, 100), dtype=int64)\n",
      "\n",
      "1st batch: target_text: tf.Tensor(\n",
      "[[58 59 56 ... 58 53 41]\n",
      " [52  1 63 ... 63 53 59]\n",
      " [61  1 50 ... 57  6  1]\n",
      " ...\n",
      " [57 43 43 ... 42  8  0]\n",
      " [46 43 43 ...  1 58 53]\n",
      " [43 47 45 ... 59  1 61]], shape=(64, 100), dtype=int64)\n"
     ]
    }
   ],
   "source": [
    "for input_text, target_text in dataset.take(1):\n",
    "    print('1st batch: input_text:', input_text)\n",
    "    print()\n",
    "    print('1st batch: target_text:', target_text)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "UkDCH15v_2I6"
   },
   "outputs": [],
   "source": [
    "# dataset shape:\n",
    "# - 172 batches\n",
    "# - 64 sequences per batch\n",
    "# - Each sequence is a tuple of 2 sub-sequences of length 100 (input_text and target_text)\n",
    "#\n",
    "#\n",
    "#     100       100           100             100       100           100\n",
    "# |/(.....)\\ /(.....)\\ ... /(.....)\\| ... |/(.....)\\ /(.....)\\ ... /(.....)\\|  <-- input_text\n",
    "# |\\(.....)/ \\(.....)/     \\(.....)/| ... |\\(.....)/ \\(.....)/     \\(.....)/|  <-- target_text\n",
    "#\n",
    "# <------------- 64 ---------------->     <------------- 64 ---------------->\n",
    "#\n",
    "# <--------------------------------- 172 ----------------------------------->"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "ghB-VwLlD-Oz"
   },
   "source": [
    "## Build the model\n",
    "\n",
    "Use [tf.keras.Sequential](https://www.tensorflow.org/api_docs/python/tf/keras/Sequential) to define the model. For this simple example three layers are used to define our model:\n",
    "\n",
    "- [tf.keras.layers.Embedding](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Embedding): The input layer. A trainable lookup table that will map the numbers of each character to a vector with `embedding_dim` dimensions;\n",
    "- [tf.keras.layers.LSTM](https://www.tensorflow.org/api_docs/python/tf/keras/layers/LSTM): A type of RNN with size units=rnn_units (You can also use a GRU layer here.)\n",
    "- [tf.keras.layers.Dense](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Dense): The output layer, with vocab_size outputs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 425
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 9280,
     "status": "ok",
     "timestamp": 1584519811503,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "0cg8DlO3QjuT",
    "outputId": "21a9daf0-1aca-4923-958e-317293023462"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tmp_input_array shape: (2, 8)\n",
      "tmp_input_array:\n",
      "[[9 0 2 1 4 6 8 8]\n",
      " [3 7 9 1 0 4 8 6]]\n",
      "\n",
      "tmp_output_array shape: (2, 8, 5)\n",
      "tmp_output_array:\n",
      "[[[-0.01348125  0.04649678  0.03382711  0.03225223  0.00077888]\n",
      "  [ 0.01548834  0.02294052 -0.03144439 -0.02458044 -0.01775809]\n",
      "  [-0.01713194  0.02779663  0.02824989 -0.01959893 -0.01335952]\n",
      "  [-0.00085108 -0.04381631  0.04934248  0.01773458 -0.0059096 ]\n",
      "  [-0.04360831  0.02180418  0.04860454 -0.04669809  0.0012654 ]\n",
      "  [-0.04296128  0.00703833 -0.01924504 -0.03515936 -0.00462629]\n",
      "  [ 0.03757649  0.01776652  0.0135636  -0.00759625  0.01923021]\n",
      "  [ 0.03757649  0.01776652  0.0135636  -0.00759625  0.01923021]]\n",
      "\n",
      " [[-0.00344205 -0.04512044 -0.04339879 -0.04655287  0.04151651]\n",
      "  [-0.01463156 -0.04065967 -0.03041167 -0.03633127 -0.04055873]\n",
      "  [-0.01348125  0.04649678  0.03382711  0.03225223  0.00077888]\n",
      "  [-0.00085108 -0.04381631  0.04934248  0.01773458 -0.0059096 ]\n",
      "  [ 0.01548834  0.02294052 -0.03144439 -0.02458044 -0.01775809]\n",
      "  [-0.04360831  0.02180418  0.04860454 -0.04669809  0.0012654 ]\n",
      "  [ 0.03757649  0.01776652  0.0135636  -0.00759625  0.01923021]\n",
      "  [-0.04296128  0.00703833 -0.01924504 -0.03515936 -0.00462629]]]\n"
     ]
    }
   ],
   "source": [
    "# Let's do a quick detour and see how Embeding layer works.\n",
    "# It takes several char indices sequences (batch) as an input.\n",
    "# It encodes every character of every sequence to a vector of tmp_embeding_size length.\n",
    "tmp_vocab_size = 10\n",
    "tmp_embeding_size = 5\n",
    "tmp_input_length = 8\n",
    "tmp_batch_size = 2\n",
    "\n",
    "tmp_model = tf.keras.models.Sequential()\n",
    "tmp_model.add(tf.keras.layers.Embedding(\n",
    "  input_dim=tmp_vocab_size,\n",
    "  output_dim=tmp_embeding_size,\n",
    "  input_length=tmp_input_length\n",
    "))\n",
    "# The model will take as input an integer matrix of size (batch, input_length).\n",
    "# The largest integer (i.e. word index) in the input should be no larger than 9 (tmp_vocab_size).\n",
    "# Now model.output_shape == (None, 10, 64), where None is the batch dimension.\n",
    "tmp_input_array = np.random.randint(\n",
    "  low=0,\n",
    "  high=tmp_vocab_size,\n",
    "  size=(tmp_batch_size, tmp_input_length)\n",
    ")\n",
    "tmp_model.compile('rmsprop', 'mse')\n",
    "tmp_output_array = tmp_model.predict(tmp_input_array)\n",
    "\n",
    "print('tmp_input_array shape:', tmp_input_array.shape)\n",
    "print('tmp_input_array:')\n",
    "print(tmp_input_array)\n",
    "print()\n",
    "print('tmp_output_array shape:', tmp_output_array.shape)\n",
    "print('tmp_output_array:')\n",
    "print(tmp_output_array)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "I7ZuvZHBD_pS"
   },
   "outputs": [],
   "source": [
    "# Length of the vocabulary in chars.\n",
    "vocab_size = len(vocab)\n",
    "\n",
    "# The embedding dimension.\n",
    "embedding_dim = 256\n",
    "\n",
    "# Number of RNN units.\n",
    "rnn_units = 1024"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "-sojdDCAICWO"
   },
   "outputs": [],
   "source": [
    "def build_model(vocab_size, embedding_dim, rnn_units, batch_size):\n",
    "    model = tf.keras.models.Sequential()\n",
    "\n",
    "    model.add(tf.keras.layers.Embedding(\n",
    "      input_dim=vocab_size,\n",
    "      output_dim=embedding_dim,\n",
    "      batch_input_shape=[batch_size, None]\n",
    "    ))\n",
    "\n",
    "    model.add(tf.keras.layers.LSTM(\n",
    "      units=rnn_units,\n",
    "      return_sequences=True,\n",
    "      stateful=True,\n",
    "      recurrent_initializer=tf.keras.initializers.GlorotNormal()\n",
    "    ))\n",
    "\n",
    "    model.add(tf.keras.layers.Dense(vocab_size))\n",
    "  \n",
    "    return model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "XoPwxyAPEg6z"
   },
   "outputs": [],
   "source": [
    "model = build_model(vocab_size, embedding_dim, rnn_units, BATCH_SIZE)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 255
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 8897,
     "status": "ok",
     "timestamp": 1584519812083,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "iLnlZFgU55bQ",
    "outputId": "eeece968-3a2a-4652-a0b7-d8f6944cdbc2"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Model: \"sequential_1\"\n",
      "_________________________________________________________________\n",
      "Layer (type)                 Output Shape              Param #   \n",
      "=================================================================\n",
      "embedding_1 (Embedding)      (64, None, 256)           16640     \n",
      "_________________________________________________________________\n",
      "lstm (LSTM)                  (64, None, 1024)          5246976   \n",
      "_________________________________________________________________\n",
      "dense (Dense)                (64, None, 65)            66625     \n",
      "=================================================================\n",
      "Total params: 5,330,241\n",
      "Trainable params: 5,330,241\n",
      "Non-trainable params: 0\n",
      "_________________________________________________________________\n"
     ]
    }
   ],
   "source": [
    "model.summary()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 422
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 8704,
     "status": "ok",
     "timestamp": 1584519812084,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "CcaO_rO_8-GH",
    "outputId": "21830f00-dc89-417a-81d8-8988354235f4"
   },
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAb0AAAGVCAIAAADls7hIAAAABmJLR0QA/wD/AP+gvaeTAAAgAElE\nQVR4nOzdeVhT17ow8LUhhBCGADJIwSCDIJPidDUIeihHa0UZnEBrW3qetor6BdSjFNAKKGirRS4W\n7FPL4bRVwQEvqMix14EKVtSKCMXWAoqonDKIDJJAEpLvj3W7by5DyE5CEuD9/eWe1lp7N7zdw1rv\nIiQSCQIAACA3HU03AAAARhmImwAAQA3ETQAAoAbiJgAAUEOTXrh161ZqaqqmmgIAANqJw+Fs27aN\nXPw/95vPnj07e/as2psEwCh29uzZ58+fa7oVI66srKysrEzTrdCMsrKyW7duSa+hDdzpzJkz6moP\nAKMeQRBbt25ds2aNphsyslavXo3Ga3DA5y4N3m8CAAA1EDcBAIAaiJsAAEANxE0AAKAG4iYAAFAD\ncRMADbh06RKLxbpw4YKmG6JiGzduJP60fv166U1XrlyJjY3F/xYKhSkpKc7OznQ63dTU1NPTs76+\nfmBpPT09U6dO3bVrlzxVi8Xi0NBQNpvNYDBsbW2Dg4MrKysRQufPn//ss8/6+vrIPfPz88lGWlhY\nKHCaEDcB0IAxnIfM3Ny8qKjo0aNHWVlZ5Mo9e/akp6fHxcXhxbCwsO++++7EiRM8Hu/XX391cnJ6\n/fr1wKLi4+MfPXokZ71isbikpOTkyZNtbW2lpaV8Pn/BggWNjY1BQUEMBiMgIKC9vR3vGRwc/Pz5\n8xs3bixdulSxc4S4CYAGBAYGdnR0LF++fKQr4vP5Pj4+I12LNAMDgyVLlri4uOjr6+M1Bw4cyM3N\nPX36tLGxMUIoNzc3Pz//zJkzc+fOpdFoNjY2BQUFnp6e/cr56aeffvnlF0pVczgcX19fJpPp4OCQ\nnJzc0dHxz3/+EyEUFRU1ffr0pUuXikQihBBBELa2tn5+flOmTFHsHCFuAjCWZWVlNTc3a7ABtbW1\nu3fvTkxMZDAYeM3Ro0dnzpzp5eUl4yg+n79jx460tDT5K6LRaNLvPRwdHRFCdXV1eDEhIaGiooJS\ngTJA3ARA3UpLS9lsNkEQX375JUIoMzPT0NCQyWQWFBS8/fbbJiYmdnZ2OTk5eOf09HQGg2FlZbVx\n40YbGxsGg+Hj43P79m28lcvl0un0iRMn4sXNmzcbGhoSBNHa2ooQio6O3r59e11dHUEQzs7OCKF/\n/etfJiYmycnJajvZ9PR0iUQSFBSEFwUCQVlZmbe3t+yj4uPjN2/ebGlpqXC9fD4fIWRiYoIXzczM\nFi5cmJaWppI3JBA3AVA3X1/fn376iVzctGnT1q1b+Xy+sbHxqVOn6urqHB0dP/roI6FQiBDicrkR\nERE8Hi8qKqq+vr68vFwkEi1atOjZs2cIofT0dOkhnhkZGYmJieRiWlra8uXLnZycJBJJbW0tQgh/\nHhGLxWo72cLCQldXVyaTiRcbGxsFAsG9e/f8/f3x/wbc3NwyMjKkw9nNmzfr6urWrVunTL137txB\nCPn6+pJrZsyY8eLFiwcPHihTLAZxEwBt4ePjY2JiYmlpGR4e3t3d3dDQQG6i0Whubm76+vru7u6Z\nmZldXV3Z2dkKVBEYGNjZ2bl7927VtVqW7u7uJ0+eODk5kWvw9x9LS8vk5OTq6uqmpqaQkJAtW7ac\nPHkS78Dn86OjozMzMxWutKmpKTc3NyoqisPhkPe5CCH8NrOqqkrhkkkQNwHQOnQ6HSGE7zcHmj17\nNpPJ/O2339TbKEU0NzdLJBLyZhMhhD8WeXh4+Pj4mJubs1isxMREFov19ddf4x3i4uI+/vhjW1tb\nhSvlcDhRUVEhISFFRUV6enrketyMpqYmhUsmDZIPCQCg5fT19VtaWjTdiuH19PSgP2MlZmNjgxDC\nr18xOp1ub2+PP+CUlpZWVVUpmQXYysoqKyvLw8Oj33oDAwOySUqC+00ARhmhUNje3m5nZ6fphgwP\nhyrpPudGRkZTpkx5+PCh9G4ikYjFYiGEsrKyrl69qqOjgzul4+9CycnJBEH8/PPPclZqaWlpamo6\ncL1AICCbpCSImwCMMsXFxRKJZN68eXiRRqMN9USvcVZWVgRBdHR0SK8MCwu7f//+48eP8SKPx3v6\n9CnulpSdnS2Rgu+p4+PjJRLJ7Nmz5az0woULgz7m42ZYW1src0YYxE0ARgGxWPzq1SuRSFRZWRkd\nHc1msyMiIvAmZ2fntra2/Px8oVDY0tLy9OlT6QPNzc0bGxvr6+u7urqEQmFRUZE6+yExmUxHR8d+\n+fC3bdtmb28fERHR0NDw8uXLmJgYPp//ySefyFNgeHi4tbV1eXn5UDvU1tZaW1uHhYUN3ISbIbvf\nqJwgbgKgbl9++eWcOXMQQjExMcHBwZmZmYcPH0YITZs27fHjx8eOHdu+fTtCaMmSJTU1NfiQnp4e\nLy8vAwMDPz8/FxeX69evky8NN23a5O/vv3btWldX17179+LnUA6HgzsqRUZGWllZubu7L126tK2t\nTf0nGxgYWF1djXtTYmZmZiUlJXZ2dt7e3ra2tnfu3CksLBy2RycmEAiam5sLCgqG2kFG98y7d+/a\n2tpOmzaNUvuHrIZ06tSpfmsAALIhhE6dOjWiVWzYsMHc3HxEqxjWqlWrVq1aNexuGzZssLW1lV5T\nU1NDo9G+//57lTSjr6/Pz88vKyuL6oGtra0MBuPQoUPSK6OioiZMmDDssQPPHe43ARgFpD+taDk+\nn3/58uWamhr8HcbZ2TkpKSkpKWnQzB2U9PX15efnd3V1hYeHUz02ISHB29uby+UihCQSSWNjY2lp\nKR4LoACImwAAVWpra8N5Pf72t7/hNbGxsatXrw4PD+/3gYiq4uLivLy8oqIi6Q6h8khNTa2oqLh0\n6RLuzllQUIDzehQWFirWEs3EzTlz5ujq6sr5RmMoH374obGxMUEQFRUV8mxVc8ZDsVh8+PBhqqlo\ntDAtY1lZmZubG+4aYm1tvW/fPrVVnZeX5+joiLukTJw4sV8+x3EiLi4uOzu7o6PDwcFB+6fp/uqr\nr8iH2ePHj5Prk5OTuVzu/v37lSk8ICDgxIkT5GB8ORUUFPT29hYXF5uZmeE1ISEh0s/vijRF+qFd\nne83AwICpk+frmQhOPfB/fv35dl68eJFExOT8+fPK1mpPH7//ff58+cjhKieozobSclbb72FEHr1\n6pX6q3ZycmKxWOqvV05o5N9vagM532+OSQPPXZPjhQiCUGd1OOOhGip68OBBUlJSZGRkd3e3hGLy\nFbU1ks/nBwQESGeX0BJa2zAASJp8vyk9dFQxsiOvCuOyRCI5c+YMOYRWtunTp+fl5b3zzjvSw8u0\njcbTMg5FaxsGAEmRuNnX1/fpp5+y2WwDA4Np06bhp/u0tDRDQ0MdHZ1Zs2ZZW1vr6ekZGhrOnDnT\nz89v0qRJDAbD1NR0586d0uXU1tZOnTrV0NAQ90orLS2VXQVCSCKRHDx40NXVVV9fn8Vi7dixQ7pA\nGVspZTzEDUhJSXF1dTUwMLCwsHBwcEhJSZFO2DUSRktaRnU2TB4lJSXu7u4sFovBYHh5eV2+fBkh\n9OGHH+IXo05OTvfv30cIffDBB0wmk8VinT9/Hg3xG/v888+ZTKaxsXFzc/P27dttbW3ln6cBjCPS\nD+1yvt/8+9//rq+vf/bs2VevXsXFxeno6Ny9e1cikezZswchdPv27e7u7tbW1iVLliCECgsLW1pa\nuru7cQ+AiooKXEhAQICjo+OTJ0+EQuEvv/wyd+5cBoPx+++/y64iPj6eIIgvvvji1atXPB4vIyMD\nSb3BlL0VdwM+cuQIuTNC6OrVqx0dHc3NzX5+foaGhgKBAG9NTk7W1dUtKCjg8Xj37t2ztrb+y1/+\nQvW1yNy5c6m+36TUyA0bNhgaGj58+LCnp6e6unrOnDnGxsYNDQ146zvvvGNtbU2WfPDgQYRQS0sL\nXly5ciVOy4hdvHjR2Ng4KSlpqIb1e7+ptoZJ5Hi/eebMmYSEhLa2tpcvX86bN4/skbdy5UpdXd0X\nL16Qe65bt458dyzjN4YQioqKOnLkyIoVK3799VcZVUvg/eY4oIL+mz09PZmZmaGhoStXrjQ1Nd21\na5eenp50KkB3d3cmkzlhwoS1a9cihNhstoWFBZPJxB9DpZNfGRsbT548mUajeXh4HDt2rKenBz8I\nD1UFn88/fPjwX//6123btpmamhoYGJibm5Olyd46lKEyHubn58+aNSsoKMjAwGDmzJnBwcE3btzA\n/dHUT2vTMqqhYfJYtWrVnj17zMzMzM3Ng4KCXr58icc1R0ZG9vX1kfV2dnbevXsXT8U17M/4wIED\nW7ZsycvLmzp16gg1G4xelL8LPXr0iMfjkZMoGRgYTJw4cdBUgDiHIJ4ICf35NnOoBAReXl4sFgvP\n2zlUFbW1tTweLyAgYNASZG8dVr+Mhz09PeR0KAihvr4+PT09XV1dxQpXFa1Ny6g9DcM/M9xL/M03\n33RxcfnHP/4RFxdHEERubm54eDj+jyj/z1geYWFhgw6IHnvU/C1Xe6xatUp6kXLc7O7uRgjt2rVL\nelJjnFNPSXp6evgPb6gq8LD8oaYckb2VqqVLlx48eLCgoGDx4sXV1dX5+fnLli3TeNwcltamZRzR\nhhUWFh48eLC6urqzs1M6dhMEsXHjxm3btl29evWvf/0rnngWb1Ltzzg6OprD4ShxBqMAHkG/detW\nTTdEA/C5S6McN3FgOnz4cHR0tGoahRBCSCQStbW1sdlsGVVcv34dIdTb2ztoCfj2cKitVCUkJNy7\ndy8iIuL169c2NjZr1qxR51RWitHatIwj0bAbN27cu3dv69atDQ0NoaGhK1as+Mc//vHGG28cOXJE\n+vNjREREXFzcN998M2nSJBMTE3t7e7xetT9jDocz0t8MNe7MmTMIoTF/moPC5y6NctzEH8cHHaKj\njOvXr4vF4pkzZ8qowtPTU0dH58cff4yMjBxYguytVFVXV9fV1bW0tNBooyYlvtamZRyJht27d8/Q\n0BAhVFVVJRQKN23ahOd97fcgaWZmFhYWlpuba2xs/NFHH5HrR+hnDMYJyt+FGAzGBx98kJOTk5mZ\n2dnZ2dfX9/z583//+98K1C0QCDo6OkQiUXl5OZfLxSn5ZFRhaWm5cuXKs2fPZmVldXZ2VlZWSneo\nlL2Vqi1btrDZbOUzEYw0rU3LqKqGDSxZKBQ2NTUVFxfjuImfUa5cudLT01NTU0N2eCJFRkb29vZe\nvHhx+fLl5EoV/ozBeCT9cV3Ofki9vb0xMTFsNptGo+FoVV1dnZaWhgfbT548uaSk5MCBAzjxvbW1\n9YkTJ3Jzc3GaZTMzs5ycHIlEkp2d7e/vb2VlRaPR8Mf3p0+fyq5CIpF0dXV9+OGHEyZMMDIy8vX1\n/fTTTxFCdnZ2Dx48kL31yJEjuM8gk8kMCgrKyMjArZ0yZUpdXd3XX3+N51m2t7fHfaGuXbs2YcIE\n8irp6em5ubnl5eXJ02vh1q1b8+fPJ1+WTZw40cfH58cffxz2QKqN3LBhg56enq2tLY1GMzExCQkJ\nqaurI0t7+fKlv78/g8FwcHD4f//v/+HerM7Ozrg/UHl5ub29vYGBga+v7x9//HHp0iVjY+N9+/YN\nbFVZWZmHh4eOjg4+l+TkZLU17OjRo9JTIfZz7tw5XGBMTIy5ubmpqenq1atx11cnJyey25NEIpkx\nY0ZsbGy/8xr0N/bZZ5/h/JWTJk2SM/UZgn5IY93Ac4f8m4PLyMiIjo4mF3t7e7du3aqvr8/j8TTY\nqn60IS3joLStYUuXLn38+PEIFQ5xc8zTrvHpWuuPP/7gcrnSL7/odDqbzRYKhUKhUCXzOqmK1qZl\n1HjDhEIh7pNUWVmJ72012x4wlkD+zUEYGBjo6ellZWU1NTUJhcLGxsZvvvnm008/DQ8Pb2xsJIYm\nO53qb7/9pvCxgKqYmJiamprff//9gw8+2Lt3r6abM15s3LiR/En3y/t35cqV2NhY/G+hUJiSkuLs\n7Eyn001NTT09Pevr6weW1tPTM3XqVOm+YjKIxeLQ0FA2m81gMGxtbYODg3F/8PPnz3/22WfS/yPP\nz88nG2lhYaHIeUrffMJzOunGjRt//etfTUxMdHV1WSyWj49PRkaGUCjUdLv+V2xsLO5tPnny5DNn\nzmi6Of9LSxoWHx+vo6MzadKkkU7Kh+A5XQp+RVNUVPTo0aOenh5y/aeffrp8+fLOzk68GBoa6urq\nWlZWhu9LgoKCqqqqBpa2bds29Od8lsMSCoUTJkwoKSnp7u5+/PjxokWLWCwWHmWblpa2cOFCcpSw\nWCx+/vz5jRs3li5dqtg8GRA3AVDKSMdNHo/H4XA0XpTC8wtJJJL9+/e7uLjw+Xy8mJOTQxBEZWWl\n7KJu3ry5ePFiSnFz2bJl5OKdO3cQQsnJyXiRy+VyOJx+tz4wvxAAY5MKE+tpJEdfbW3t7t27ExMT\nyYHLR48enTlzpuz5ePl8/o4dO9LS0uSviEajSU+UgPvz1tXV4cWEhISKigpKBcoAcROAESeRSFJT\nU3GiEzMzs5CQEHIsPKXEehpMHqiw9PR0iUQSFBSEFwUCQVlZ2bBz5MTHx2/evFmZYdN45mHcQw4h\nZGZmtnDhwrS0NAnFVOKDgrgJwIhLSEiIjY2Nj49vbm6+cePGs2fP/Pz8mpqaEELp6enSgxczMjIS\nExPJxbS0tOXLl+PEerW1tVwuNyIigsfjRUVF1dfXl5eXi0SiRYsW4fSDlIpCf/Z5EIvFI3ruhYWF\nrq6u5ExqjY2NAoHg3r17/v7+OPS7ubllZGRIh7ObN2/W1dWtW7dOmXrxc7qvry+5ZsaMGS9evHjw\n4IEyxWIQNwEYWXw+PzU1dcWKFevXr2exWF5eXl999VVra6vC49k0mzyQku7u7idPnkgPXsBj8Cwt\nLZOTk6urq5uamkJCQrZs2XLy5Em8A5/Pj46OzszMVLjSpqam3NzcqKgoDodD3ucihKZMmYIQqqqq\nUrhkEsRNAEZWdXX169evZ8+eTa6ZM2cOnU4fOCRUARpMHiiP5uZmiUQiPW0vnjzGw8PDx8fH3Nyc\nxWIlJiayWCzy/yJxcXEff/yxra2twpVyOJyoqKiQkJCioiLpyXhwM/BtvpKg3zsAI6u9vR0hZGRk\nJL3S1NS0q6tLJeVrbfJAhFBPTw/6M1ZiePyx9Oy7dDrd3t4ef8ApLS2tqqpKTU1VplIrK6usrCwP\nD49+6/GIFdwkJcH9JgAjy9TUFCHUL0qqKrGe1iYPxHCoku5zbmRkNGXKlIcPH0rvJhKJcDqLrKys\nq1ev6ujo4E7p+LtQcnIyQRA///yznJVaWlria94Pnq9BJeP9IG4CMLI8PT2NjIyk/+xv374tEAhm\nzZqFF5VJrKe1yQMxKysrgiD6TW0dFhZ2//79x48f40Uej/f06VPcLSk7O1u6myS+j8b9N6VfdMh2\n4cKFQR/zcTNwgiElQdwEYGQxGIzt27efO3fu+PHjnZ2dVVVVkZGRNjY2GzZswDtQTayntckDB2Iy\nmY6OjnguBtK2bdtw0siGhoaXL1/GxMTw+fxPPvlEngLDw8Otra3Ly8uH2qG2ttba2nrQaUtwM2T3\nG5UTxE0ARtyePXtSUlKSkpIsLCwWLlw4efJkMn8oQmjTpk3+/v5r1651dXXdu3cvfpDkcDi4d1Fk\nZKSVlZW7u/vSpUvb2toQQj09PV5eXnj2bBcXl+vXr5MvEKkWpQaBgYHV1dW4NyVmZmZWUlJiZ2fn\n7e1ta2t7586dwsLCYXt0YgKBoLm5uaCgYKgdZHTPvHv3rq2t7bRp0yi1f8hqSDDOEgCqkHrHp2sq\nR5/C4yxrampoNJqcyUyH1dfX5+fnl5WVRfXA1tZWBoNx6NAh6ZUwzhKA8ULjOfpk4/P5ly9frqmp\nwd9hnJ2dk5KSkpKSlJ89oa+vLz8/v6urS4HkYQkJCd7e3lwuFyEkkUgaGxtLS0tx/38FQNwEAKhS\nW1vbkiVLXFxc/va3v+E1sbGxq1evDg8P7/eBiKri4uK8vLyioiLpDqHySE1NraiouHTpEu7OWVBQ\nYGtr6+fnV1hYqFhLIG4CMGrExcVlZ2d3dHQ4ODicPXtW080ZxFdffUU+zB4/fpxcn5yczOVy9+/f\nr0zhAQEBJ06cIAfgy6mgoKC3t7e4uNjMzAyvCQkJkX5+V6Al0O8dgFEjJSUlJSVF061Q0OLFi3Fe\nODULDg4ODg5WbZlwvwkAANRA3AQAAGogbgIAADUQNwEAgJpBvgudPn1a/e0AYPS6deuWppsw4vAg\nxfEZHJ4/f94/c4p0J3g8XggAAIC0fuOFCIkqZtsAQOUIgjh16pT0xA8AaAl4vwkAANRA3AQAAGog\nbgIAADUQNwEAgBqImwAAQA3ETQAAoAbiJgAAUANxEwAAqIG4CQAA1EDcBAAAaiBuAgAANRA3AQCA\nGoibAABADcRNAACgBuImAABQA3ETAACogbgJAADUQNwEAABqIG4CAAA1EDcBAIAaiJsAAEANxE0A\nAKAG4iYAAFADcRMAAKiBuAkAANRA3AQAAGogbgIAADUQNwEAgBqImwAAQA3ETQAAoAbiJgAAUANx\nEwAAqIG4CQAA1BASiUTTbQAAIYQ2bNjw6NEjcrG8vNzBwcHMzAwv6urqfvvtt3Z2dhpqHQD/i6bp\nBgDwP6ytrb/++mvpNZWVleS/HR0dIWgCLQHP6UBbrFu3bqhNdDo9IiJCjW0BQBZ4TgdaxNPT8+HD\nh4P+Jh89euTi4qL+JgEwENxvAi3y3nvv6erq9ltJEMT06dMhaALtAXETaJG1a9f29fX1W6mrq/v+\n++9rpD0ADAqe04F28fHxuX37tlgsJtcQBPHs2TNbW1sNtgoAaXC/CbTLu+++SxAEuaijo+Pr6wtB\nE2gViJtAu6xevVp6kSCI9957T1ONAWBQEDeBdrGwsAgICCC/DhEEERoaqtkmAdAPxE2gddavX49f\nu+vq6r711lsTJkzQdIsA+D8gbgKts2LFCjqdjhCSSCTr16/XdHMA6A/iJtA6hoaGy5YtQwjR6fTl\ny5drujkA9AdxE2ijd955ByEUGhpqaGio6bYAMIBEPqtWrdJ0SwEAYGTJGQ8p5EOaN2/e1q1bR67F\nAEg7fvx4eHg4jUY5Zdfhw4cRQmP+t3rr1q20tLRTp05puiFjBL6ecu5M4UdpZ2e3Zs0ahZoEAGVB\nQUEMBkOBA8+cOYMQGg+/1bS0tPFwmmojf9yE95tASykWNAFQA4ibAABADcRNAACgBuImAABQA3ET\nAACogbgJAEIIXbp0icViXbhwQdMN0UZXrlyJjY3F/xYKhSkpKc7OznQ63dTU1NPTs76+fuAhPT09\nU6dO3bVrlzzli8Xi0NBQNpvNYDBsbW2Dg4PxlHznz5//7LPPBqay1jiImwAghBAk8B7Knj170tPT\n4+Li8GJYWNh333134sQJHo/366+/Ojk5vX79euBR8fHx0rM6yyYWi0tKSk6ePNnW1lZaWsrn8xcs\nWNDY2Ij7ogUEBLS3t6vsfFQB4iYACCEUGBjY0dGhhuHwfD7fx8dnpGtRlQMHDuTm5p4+fdrY2Bgh\nlJubm5+ff+bMmblz59JoNBsbm4KCAk9Pz35H/fTTT7/88gulijgcjq+vL5PJdHBwSE5O7ujo+Oc/\n/4kQioqKmj59+tKlS0UikYrOSQUgbgKgVllZWc3NzZpuhVxqa2t3796dmJhI9qU9evTozJkzvby8\nZBzF5/N37Nghfx9yhBCNRpN+Q+Lo6IgQqqurw4sJCQkVFRWUChxpEDcBQKWlpWw2myCIL7/8EiGU\nmZlpaGjIZDILCgrefvttExMTOzu7nJwcvHN6ejqDwbCystq4caONjQ2DwcBzIuGtXC6XTqdPnDgR\nL27evNnQ0JAgiNbWVoRQdHT09u3b6+rqCIJwdnZGCP3rX/8yMTFJTk7WwGkPJz09XSKRBAUF4UWB\nQFBWVubt7S37qPj4+M2bN1taWipcL5/PRwiZmJjgRTMzs4ULF6alpWnPuxSImwAgX1/fn376iVzc\ntGnT1q1b+Xy+sbHxqVOn6urqHB0dP/roI6FQiBDicrkRERE8Hi8qKqq+vr68vFwkEi1atOjZs2cI\nofT0dOmxjxkZGYmJieRiWlra8uXLnZycJBJJbW0tQgh/9JCeh057FBYWurq6MplMvNjY2CgQCO7d\nu+fv74//h+Hm5paRkSEdzm7evFlXV7du3Tpl6r1z5w5CyNfXl1wzY8aMFy9ePHjwQJliVQjiJgBD\n8vHxMTExsbS0DA8P7+7ubmhoIDfRaDQ3Nzd9fX13d/fMzMyurq7s7GwFqggMDOzs7Ny9e7fqWq0a\n3d3dT548cXJyItfg7z+WlpbJycnV1dVNTU0hISFbtmw5efIk3oHP50dHR2dmZipcaVNTU25ublRU\nFIfDIe9zEUJTpkxBCFVVVSlcsmpB3ARgeDj/PL7fHGj27NlMJvO3335Tb6NGVnNzs0QiIW82EUL6\n+voIIQ8PDx8fH3NzcxaLlZiYyGKxvv76a7xDXFzcxx9/rMzkoxwOJyoqKiQkpKioSE9Pj1yPm9HU\n1KRwyapFOUkXAGAgfX39lpYWTbdClXp6etCfsRKzsbFBCOEXtRidTre3t8cfcEpLS6uqqlJTU5Wp\n1MrKKisry8PDo996AwMDsknaAO43AVCWUChsb2+3s7PTdENUCYcq6T7nRkZGU6ZMefjwofRuIpGI\nxWIhhLKysq5evaqjo0MQBEEQ+LtQcnIyQRA///yznJVaWlqampoOXC8QCMgmaQOImwAoq7i4WCKR\nzJs3Dy/SaLShnuhHESsrK4IgOjo6pFeGhYXdv3//8ePHeJHH4z19+hR3S8rOzpbOiI7vvuPj4yUS\nyezZs+Ws9MKFC4M+5uNmWFtbK3NGKgRxEwBFiMXiV69eiUSiysrK6OhoNpsdERGBNzk7O7e1teXn\n5wuFwpaWlqdPn0ofaG5u3tjYWF9f39XVJRQKi4qKtLMfEpPJdHR0fP78ucULfuUAACAASURBVPTK\nbdu22dvbR0RENDQ0vHz5MiYmhs/nf/LJJ/IUGB4ebm1tXV5ePtQOtbW11tbWYWFhAzfhZsjuN6pO\nEDcBQF9++eWcOXMQQjExMcHBwZmZmXiyjWnTpj1+/PjYsWPbt29HCC1ZsqSmpgYf0tPT4+XlZWBg\n4Ofn5+Licv36dfJV4KZNm/z9/deuXevq6rp37178dMnhcHBHpcjISCsrK3d396VLl7a1tWnkfOUU\nGBhYXV2Ne1NiZmZmJSUldnZ23t7etra2d+7cKSwsHLZHJyYQCJqbmwsKCobaQUb3zLt379ra2k6b\nNo1S+0eQ/POyrVq1Ss6dAdAgNfxWN2zYYG5uPqJVDAvPLDSiVdTU1NBotO+//14lpfX19fn5+WVl\nZVE9sLW1lcFgHDp0SCXNGAql6wn3mwAoQguT9Kics7NzUlJSUlLSoJk7KOnr68vPz+/q6goPD6d6\nbEJCgre3N5fLVbINKqR1cXPOnDm6urpy3vkP5cMPPzQ2NiYIoqKiQp6tas4hJhaLDx8+TCm5Q15e\nnqOjIzGYyZMnK9CG8XCdgfJiY2NXr14dHh7e7wMRVcXFxXl5eUVFRdIdQuWRmppaUVFx6dIl6e6c\nGqd1cfPu3bv+/v5KFvLNN98cO3ZM/q0SNY57rampWbBgwbZt23g8nvxHrVy58vHjx05OTiwWCz8p\niEQiHo/X1NRE9YeIjfnrPHLi4uKys7M7OjocHBzOnj2r6eaMuOTkZC6Xu3//fmUKCQgIOHHiBDls\nX04FBQW9vb3FxcVmZmbK1K5yWtrvnSAIdVaHc4ipoaIHDx4kJSVFRkZ2d3crGUR0dXUNDAwMDAxc\nXFwULmSsXucRlZKSkpKSoulWqNXixYsXL16s/nqDg4ODg4PVX++wtO5+E1P+nlx2RFBhvJBIJGfO\nnCGHmsk2ffr0vLy8d955R3oYhpLy8/MVPnasXmcARpSK42ZfX9+nn37KZrMNDAymTZuGP1GlpaUZ\nGhrq6OjMmjXL2tpaT0/P0NBw5syZfn5+kyZNYjAYpqamO3fulC6ntrZ26tSphoaGuJ9HaWmp7CoQ\nQhKJ5ODBg66urvr6+iwWa8eOHdIFythKKYcYbkBKSoqrq6uBgYGFhYWDg0NKSop0ChyFKZlSDK4z\nAGoi53d3Oft2/P3vf9fX1z979uyrV6/i4uJ0dHTu3r0rkUj27NmDELp9+3Z3d3dra+uSJUsQQoWF\nhS0tLd3d3fhLWUVFBS4kICDA0dHxyZMnQqHwl19+mTt3LoPB+P3332VXER8fTxDEF1988erVKx6P\nl5GRgRC6f/8+Pkr2Vtyx7siRI+TOCKGrV692dHQ0Nzf7+fkZGhoKBAK8NTk5WVdXt6CggMfj3bt3\nz9ra+i9/+Yucl5E0d+7c6dOn91t58eJFY2PjpKSkoY6Sfr8pkUiioqKqqqqkd4DrLBk3febU0A9p\nXKF0PVUZN/l8PpPJDA8Px4s8Hk9fX3/Tpk2SP/+eu7q68KZvv/0WIUT+zeN0e7m5uXgxICBAOqbg\nGZr+/ve/y6iCx+MxmcxFixaRR+E7F/wXK3urZIi/Zz6fjxfxH39tbS1enDNnzn/8x3+QRX388cc6\nOjq9vb1yXMX/NWjcHJZ0Ui9s0Lg5zq8zxE2gAErXU5XfhR49esTj8cjJRgwMDCZOnDhoci2clYuc\nMAS/ZRtqSK+XlxeLxcJ/1UNVUVtby+PxAgICBi1B9tZh9csh1tPTQ04bgBDq6+vT09PT1dVVrHCq\nWCwWOUdVdHS07J3H7XV+/vz56dOnFWvGaHHr1i2E0Jg/TbXB11NOqoyb3d3dCKFdu3ZJT/6Jc08p\nSU9PD/85DVUFHr46VGp+2VupWrp06cGDBwsKChYvXlxdXZ2fn79s2TK1xU1pKp9xZcxc57KyskGH\nOY894+Q0tY0qvwvhP5jDhw9L39BSiuKDEolEbW1tbDZbRhX4xqS3t3fQEmRvpSohIeHNN9+MiIgw\nMTFZsWLFmjVrZPRhHEXG0nWG53RAFfnlUx6qjJv4o+2gQ0eUcf36dbFYPHPmTBlVeHp66ujo/Pjj\nj4OWIHsrVdXV1XV1dS0tLUKhsKGhITMzU7Odcv/9739/8MEHypcD1xkAOakybjIYjA8++CAnJycz\nM7Ozs7Ovr+/58+f//ve/FShKIBB0dHSIRKLy8nIul4tTV8mowtLScuXKlWfPns3Kyurs7KysrJTu\n6Cd7K1Vbtmxhs9nKj9gdiGpKMYlEwufz8/LyyJn/qBqf1xkAZcl5EyvnN8re3t6YmBg2m02j0fBf\nUXV1dVpaGh4LOHny5JKSkgMHDuAE0dbW1idOnMjNzcXpSM3MzHJyciQSSXZ2tr+/v5WVFY1GmzBh\nwtq1a58+fSq7ColE0tXV9eGHH06YMMHIyMjX1/fTTz9FCNnZ2T148ED21iNHjuDhX0wmMygoKCMj\nA7d2ypQpdXV1X3/9NY5K9vb2uI/OtWvXJkyYQF5APT09Nze3vLw8eS7jrVu35s+fT77znThxoo+P\nz48//oi3Xrp0ydjYeN++fQMPPHfu3MCP6aRdu3ZJJBK4zhh8TwcK0Fg/pHEiIyMjOjqaXOzt7d26\ndau+vj6Px9Ngq8Yeha/zOPmtQtxULY31QxoP/vjjDy6XK/3ij06ns9lsoVAoFAq1Z/6T0Q6uM9Bm\nWjo+XWsZGBjo6ellZWU1NTUJhcLGxsZvvvnm008/DQ8Pb2xsHDTPG6ZA2sHxTMZ1VvhlLgCqAnGT\nGhaL9cMPP/zyyy8uLi4GBgbu7u7Z2dkHDhz49ttvp06dKuPGPjc3V9NtH01kXGdNN220unLlSmxs\nLP63UChMSUlxdnam0+mmpqaenp719fUDD+np6Zk6dap0H14ZxGJxaGgom81mMBi2trbBwcF4DMWw\nkpKS3N3dTUxM9PX1nZ2dd+7cKf0xcN++ff1uQcjhGLLP5fz585999tkIpZeG53TK/Pz8/vu//1vT\nrRj74Dqr0J49e+7fv3/ixAm8GBYW9vDhwxMnTsyaNaulpWXjxo2D9luIj49/9OiRnFWIxeKSkpL8\n/PyZM2c2NTVt2LBhwYIFDx8+fOONN2QfeO3atS1btoSHh+vp6RUVFa1fv76qqqqoqEjOeoc6l6Cg\noCdPngQEBOTn5w86t7BS5HwPOk7etYMxQA2/VR6Px+FwNFuU/N8x9u/f7+LiQuYByMnJIQiisrJS\n9lE3b97EOTfxXL7DEgqFy5YtIxdxMoTk5ORhDwwMDBSJROQiTnnV0NCAF/fu3StjgqNhz4XL5XI4\nHKFQOGwzYH4hAEZWVlZWc3OzthU1qNra2t27dycmJpKD/Y8ePTpz5kzZc+ry+fwdO3ZQGshLo9Gk\nZ0BxdHRECNXV1Q174MWLF6WHz1pYWCCE5JwNYdhzSUhIqKioUPmIZIibYJySSCSpqalubm76+vpm\nZmYhISFkDhoul0un08lJHTZv3mxoaEgQRGtrK0IoOjp6+/btdXV1BEE4Ozunp6czGAwrK6uNGzfa\n2NgwGAwfH5/bt28rUBRSOgfrQOnp6RKJJCgoCC8KBIKysrJh55WKj4/fvHmzMqkG8OzBCnzEe/Hi\nhYGBgYODw7B7ynMuZmZmCxcuTEtLk6h0jhaIm2CcSkhIiI2NjY+Pb25uvnHjxrNnz/z8/JqamhBC\n6enp0gmSMzIyEhMTycW0tLTly5c7OTlJJJLa2loulxsREcHj8aKiourr68vLy0Ui0aJFi3DSPEpF\noT+nyRSLxao6zcLCQldXV3ISqsbGRoFAcO/ePX9/fxzl3dzcMjIypMPKzZs36+rq1q1bp0y9+Dnd\n19eX0lE8Hu/atWsfffQRTo6FxcbGmpmZ0el0BweHkJCQu3fvyn8uCKEZM2a8ePHiwYMHypxOPxA3\nwXjE5/NTU1NXrFixfv16Fovl5eX11Vdftba2KjwwlEaj4VtXd3f3zMzMrq6u7OxsBcoJDAzs7Ozc\nvXu3Ys3op7u7+8mTJ9IjzfA3E0tLy+Tk5Orq6qamppCQkC1btpw8eRLvwOfzo6OjMzMzFa60qakp\nNzc3KiqKw+GQ97lySklJsbGx2bdvH7nm/fffP3/+/LNnz16/fp2Tk9PQ0LBw4cLq6mp5zgWbMmUK\nQqiqqkrhMxoI4iYYj6qrq1+/fj179mxyzZw5c+h0Ovl8rYzZs2czmcxBM8+qWXNzs0QikZ7xFE9s\n5eHh4ePjY25uzmKxEhMTWSwW+T+MuLi4jz/+2NbWVuFKORxOVFRUSEhIUVERpQmszp07d/r06cuX\nLxsbG5MrJ02aNGPGDCMjIzqdPm/evOzsbD6fj1NcD3suGD59/CShKtAPCYxHOPezkZGR9EpTU9Ou\nri6VlK+vr9/S0qKSopTR09OD/owvGM6NgN+uYnQ63d7eHn/AKS0traqqSk1NVaZSKyurrKwsDw8P\nSkfl5uampqYWFxfL7rfk5eWlq6v7+++/o+HOhYRHl+FLoSpwvwnGI9yhr1+UbG9vt7OzU75woVCo\nqqKUhEOGdN9vIyOjKVOmPHz4UHo3kUiEU8BkZWVdvXpVR0cH9zDH34WSk5MJgvj555/lrNTS0pJq\nf8kjR44cP3782rVrw3b2FIvFYrEY/59A9rmQBAIB+vNSqArETTAeeXp6GhkZSceC27dvCwSCWbNm\n4UUajTbUhCLDKi4ulkgk8+bNU74oJVlZWREE0W/O+rCwsPv37z9+/Bgv8ni8p0+f4q482dnZ0r0U\n8S0z7r8p/U5DtgsXLsj/mC+RSGJiYqqqqvLz8/vd/mNvvfWW9CKeHJDD4Qx7LiR8+jgZmKpA3ATj\nEYPB2L59+7lz544fP97Z2VlVVRUZGWljY7Nhwwa8g7Ozc1tbW35+vlAobGlpefr0qfTh5ubmjY2N\n9fX1XV1dOCaKxeJXr16JRKLKysro6Gg2m40zmVItimoOVtmYTKajoyOev4S0bds2nGi1oaHh5cuX\nMTExfD7/k08+kafA8PBwa2vr8vLyoXaora21trbuN3uHjKMePnz4+eefHzt2TE9PT3ow5aFDh/AO\nL168yM3NbW9vFwqFt27d+vDDD9lsdmRkpPzngk9fdn9VqiBugnFqz549KSkpSUlJFhYWCxcunDx5\ncnFxsaGhId66adMmf3//tWvXurq67t27Fz/lcTgc3LsoMjLSysrK3d196dKlbW1tCKGenh4vLy88\nDb2Li8v169fJt4pUi1KtwMDA6upq3JsSMzMzKykpsbOz8/b2trW1vXPnTmFh4bA9OjGBQNDc3FxQ\nUDDUDoN2k5Rx1LDdKpcsWbJr1y47Ozsmk7lmzZr58+eXlZWRiVnlOZe7d+/a2tpOmzZt+NOTn5zj\nimCcJRgt1P9b3bBhg7m5uTprlMg9LrCmpoZGo8kYqkhJX1+fn59fVlaWGo5SidbWVgaDcejQoWH3\nhHGWAKjbCOXdUZ6zs3NSUlJSUpLyM4709fXl5+d3dXVRSoqo2FGqkpCQ4O3tzeVyVVssxE0AxrjY\n2NjVq1eHh4f3+0BEVXFxcV5eXlFRkXSH0BE6SiVSU1MrKiouXbpEqRupPCBuAqCUuLi47Ozsjo4O\nBweHs2fParo5g0tOTuZyufv371emkICAgBMnTpBj7Uf0KOUVFBT09vYWFxePxDSo0O8dAKWkpKSk\npKRouhXDW7x4Mc4LN04EBwcHBwePUOFwvwkAANRA3AQAAGogbgIAADUQNwEAgBoK34XKyspWr149\nck0BQCXKysoQQmP+t4qHD47501SbfqNRZSMk8qWPT01NvXXrlqJNAoCyoqKiGTNmqL//ChjPzpw5\nI89u8sZNANSMIIhTp05JTzIBgJaA95sAAEANxE0AAKAG4iYAAFADcRMAAKiBuAkAANRA3AQAAGog\nbgIAADUQNwEAgBqImwAAQA3ETQAAoAbiJgAAUANxEwAAqIG4CQAA1EDcBAAAaiBuAgAANRA3AQCA\nGoibAABADcRNAACgBuImAABQA3ETAACogbgJAADUQNwEAABqIG4CAAA1EDcBAIAaiJsAAEANxE0A\nAKAG4iYAAFADcRMAAKiBuAkAANRA3AQAAGogbgIAADUQNwEAgBqaphsAwP9ob2+XSCTSa7q7u1+9\nekUuGhkZ6enpqb1dAPRH9PulAqApb7755vXr14faqqur++LFC2tra3U2CYBBwXM60BZr164lCGLQ\nTTo6OgsWLICgCbQExE2gLVatWkWjDf7iiCCI9957T83tAWAoEDeBtjAzM1u8eLGuru7ATTo6OqGh\noepvEgCDgrgJtMj69evFYnG/lTQaLTAwkMViaaRJAAwEcRNokaCgIH19/X4r+/r61q9fr5H2ADAo\niJtAizCZzNDQ0H6djQwMDJYuXaqpJgEwEMRNoF3WrVsnFArJRT09vVWrVhkYGGiwSQD0A3ETaJe3\n3npL+lWmUChct26dBtsDwEAQN4F20dPTCw8Pp9PpeNHU1DQgIECzTQKgH4ibQOusXbtWIBAghPT0\n9NavXz9Up04ANAXGWQKtIxaL33jjjaamJoRQaWnp/PnzNd0iAP4PuN8EWkdHR+fdd99FCNnY2Pj4\n+Gi6OQD0N+qfgE6fPq3pJgDVs7CwQAjNnTv3zJkzmm4LUD0fHx87OztNt0Jxo/45fahMEAAArXXq\n1Kk1a9ZouhWKG/X3m2j0/zfQWgRBaPDanj17dtWqVWqoaPXq1QghuLFVmzFwrwPvN4GWUk/QBEAB\nEDcBAIAaiJsAAEANxE0AAKAG4iYAAFADcRMAAKiBuAlU7NKlSywW68KFC5puiJpcuXIlNjYW/1so\nFKakpDg7O9PpdFNTU09Pz/r6+oGH9PT0TJ06ddeuXfKULxaLQ0ND2Ww2g8GwtbUNDg6urKyU58Ck\npCR3d3cTExN9fX1nZ+edO3e+fv2a3Lpv3z7i//L09JQ+fKhzOX/+/GeffdbX1ydPG8YqiJtAxUb7\nSApK9uzZk56eHhcXhxfDwsK+++67EydO8Hi8X3/91cnJSTpUkeLj4x89eiRnFWKxuKSk5OTJk21t\nbaWlpXw+f8GCBY2NjcMeeO3atS1bttTX17e2tqakpKSlpeGeqnIa6lyCgoIYDEZAQEB7e7v8pY01\nklEOIXTq1ClNt2Js0vJry+PxOByO8uWsWrVq1apVChy4f/9+FxcXPp+PF3NycgiCqKyslH3UzZs3\nFy9ejBCKj4+XpxahULhs2TJy8c6dOwih5OTkYQ8MDAwUiUTkIh6/0NDQgBf37t37/fffD3XssOfC\n5XI5HI5QKJTnFPrR8t+VPOB+E4xWWVlZzc3Nmqq9trZ29+7diYmJDAYDrzl69OjMmTO9vLxkHMXn\n83fs2JGWliZ/RTQaTfqlh6OjI0Korq5u2AMvXrwoPTkoHvLP4/HkqXTYc0lISKioqKB0ImMJxE2g\nSqWlpWw2myCIL7/8EiGUmZlpaGjIZDILCgrefvttExMTOzu7nJwcvHN6ejqDwbCystq4caONjQ2D\nwfDx8bl9+zbeyuVy6XT6xIkT8eLmzZsNDQ0JgmhtbUUIRUdHb9++va6ujiAIZ2dnhNC//vUvExOT\n5ORk9Zxpenq6RCIJCgrCiwKBoKyszNvbW/ZR8fHxmzdvtrS0VLhePp+PEDIxMaF64IsXLwwMDBwc\nHIbdU55zMTMzW7hwYVpammQ8vZYhQdwEquTr6/vTTz+Ri5s2bdq6dSufzzc2Nj516lRdXZ2jo+NH\nH32EZxDicrkRERE8Hi8qKqq+vr68vFwkEi1atOjZs2cIofT0dOmh8RkZGYmJieRiWlra8uXLnZyc\nJBJJbW0tQgh/qRg4jfAIKSwsdHV1ZTKZeLGxsVEgENy7d8/f3x//P8DNzS0jI0M6rNy8ebOurk7J\naT/wc7qvry+lo3g83rVr1z766CMykT5CKDY21szMjE6nOzg4hISE3L17V/5zQQjNmDHjxYsXDx48\nUOZ0RimIm0AdfHx8TExMLC0tw8PDu7u7GxoayE00Gs3NzU1fX9/d3T0zM7Orqys7O1uBKgIDAzs7\nO3fv3q26Vg+pu7v7yZMnTk5O5Br8zcTS0jI5Obm6urqpqSkkJGTLli0nT57EO/D5/Ojo6MzMTIUr\nbWpqys3NjYqK4nA45H2unFJSUmxsbPbt20euef/998+fP//s2bPXr1/n5OQ0NDQsXLiwurpannPB\npkyZghCqqqpS+IxGL4ibQK3w/Y70jJXSZs+ezWQyf/vtN/U2irLm5maJRELebCKE8LTvHh4ePj4+\n5ubmLBYrMTGRxWJ9/fXXeIe4uLiPP/7Y1tZW4Uo5HE5UVFRISEhRUVG/qZJlO3fu3OnTpy9fvmxs\nbEyunDRp0owZM4yMjOh0+rx587Kzs/l8fkZGhjznguHTx2n5x5uxkEcOjCX6+votLS2absUwenp6\n0J/xBbOxsUEI4XevGJ1Ot7e3xx9wSktLq6qqUlNTlanUysoqKyvLw8OD0lG5ubmpqanFxcVvvPGG\njN28vLx0dXV///13NNy5kPDkzPhSjDdwvwm0iFAobG9v1/5M4DhkSPf9NjIymjJlysOHD6V3E4lE\neE7jrKysq1ev6ujo4B7m+LtQcnIyQRA///yznJVaWlqamppSaueRI0eOHz9+7do12UETISQWi8Vi\nMf4/gexzIeG588bn1PYQN4EWKS4ulkgk8+bNw4s0Gm2oJ3rNsrKyIgiio6NDemVYWNj9+/cfP36M\nF3k83tOnT3FXnuzsbOnef/iGGvffnD17tpyVXrhwQf7HfIlEEhMTU1VVlZ+fb2RkNHCHt956S3rx\n7t27EomEw+EMey4kfPrW1tZyNmksgbgJNEwsFr969UokElVWVkZHR7PZ7IiICLzJ2dm5ra0tPz9f\nKBS2tLQ8ffpU+kBzc/PGxsb6+vquri6hUFhUVKS2fkhMJtPR0fH58+fSK7dt22Zvbx8REdHQ0PDy\n5cuYmBg+n//JJ5/IU2B4eLi1tXV5eflQO9TW1lpbW4eFhcl51MOHDz///PNjx47p6elJD6Y8dOgQ\n3uHFixe5ubnt7e1CofDWrVsffvghm82OjIyU/1zw6cvurzpWQdwEqvTll1/OmTMHIRQTExMcHJyZ\nmXn48GGE0LRp0x4/fnzs2LHt27cjhJYsWVJTU4MP6enp8fLyMjAw8PPzc3FxuX79OvnecNOmTf7+\n/mvXrnV1dd27dy9+JORwOLijUmRkpJWVlbu7+9KlS9va2tR8poGBgdXV1bg3JWZmZlZSUmJnZ+ft\n7W1ra3vnzp3CwsJhe3RiAoGgubm5oKBgqB0G7SYp46hhu1UuWbJk165ddnZ2TCZzzZo18+fPLysr\nmzBhgvzncvfuXVtb22nTpg1/emOPGscmjQg0+sdsaS01XNsNGzaYm5uPaBXDUmycZU1NDY1GkzFU\nkZK+vj4/P7+srCw1HKUSra2tDAbj0KFDChw7Bv5m4X4TaNgozazj7OyclJSUlJQ0aOYOSvr6+vLz\n87u6usLDw0f6KFVJSEjw9vbmcrnqr1objP24eejQIfwW/6uvvlJ/7Xl5eY6OjvjV0sSJE9evXz/U\nng8ePAgPD3dwcNDX17ewsJg+fTrZSzk8PJyQ6eLFi9IVDdX3OzU1lSAIHR2dqVOn3rhxY0ROeDyJ\njY1dvXp1eHh4vw9EVBUXF+fl5RUVFUl3CB2ho1QiNTW1oqLi0qVLlLqRjimavuFVFpLjnh+/Sjt6\n9Kh6mjSQk5MTi8WSsUNlZSWTyYyKinry5Amfz3/06NHOnTsDAgLw1rCwsB9++AG/wv/3v/+NEAoK\nChIIBN3d3c3NzR999NGFCxfIihBCEydOFAgE/aoQiUT29vYIIbLYYclzbZURGxuLu8FPnjz5zJkz\nI1eRbArnQ8IuX74cExOjwvZoufz8/JSUFOlMS1SN9O9KDcb+/aac+Hy+j4+Ppmo/dOiQqalpWlra\n5MmTGQyGi4sL+RkEIUQQxPz581ksFo1GI9fo6ekxmUxLS8tZs2ZJFzVr1qw//vgjPz+/XxV5eXnK\nDFYZCSkpKb29vRKJ5MmTJ6N31t/FixcfOHBA061Qn+Dg4NjYWOlMS+MQxM3/odmkZC9fvuzo6JD+\nKEyn08nsYTk5OTKexTZs2LBs2TJycdOmTQiho0eP9tstNTUVf8sGAChpPMbNH3/88T/+4z+YTKaJ\niYmXl1dnZ2e/pGRpaWmGhoY6OjqzZs2ytrbW09MzNDScOXOmn5/fpEmTGAyGqanpzp07yQKVz2A2\nZ86c7u7uN9988+bNm0qe3Ztvvunm5nb9+nXpjOI3b97k8Xg4XS4AQEnjLm52d3cHBQWtWrWqra2t\npqbGxcVFIBD0S0oWHR29Y8cOiURy9OjRJ0+e/PHHHwsWLLh//35sbOz9+/fb2tref//9gwcPkhm0\nlM9gtnPnztmzZz948MDX19fDw+Pzzz9XpkPixo0bEULS38G++OKLbdu2KVwgAEDauIub9fX1nZ2d\nHh4eDAbD2to6Ly8P58EelLu7O5PJnDBhwtq1axFCbDbbwsKCyWTiz+Jk2h7lM5gZGBj89NNP//mf\n/zl16tSHDx/GxMS4ubn9+OOPipX2/vvvGxoafvvtt7hX9uPHj+/evatk2kcAAGnc5UNydHS0srJa\nv359VFRURETE5MmT5TkKf/YViUR4EXe/UO3QaT09PS6Xy+Vyb9++feDAgfz8/NWrVz969MjMzIxq\nUSwWa926dceOHcvNzf3ggw8OHz68adMmOp2OEzFQcvjw4TNnzlA9anQpKytDCFGaswyMc+PuftPA\nwODatWu+vr7JycmOjo7h4eHSQ+W0wdy5c//rv/4rMjKypaXl+vXrihWCvw599dVX7e3tZ86cwU/u\nAACVGHf3mwghDw+PCxcutLS0pKamHjhwwMPDQz1Jwvu5cePGvXv3tm7dihBauXLlqVOnyG5GCKF3\n33336NGjcs6iNZC3t/e8efPKyso2bNiwevVqBW5asa1bt0pPVjEmqh5d5QAAIABJREFU4TvNMX9b\nrT0IgtB0E5Q17u43GxsbcWJBS0vL/fv3z5w5s1+eQbW5d++eoaEh/ndvb2+/ZuCv4cokTcC3nGfP\nnsWhGQCgKuMxbm7cuPG3334TCAT3799/+vQpzvbYLykZpTKpZjATCoVNTU3FxcVk3EQIhYaGnj59\nur29vaOjo6Cg4JNPPgkODlYmbq5Zs8bCwiI0NBTPHAsAUBkNj1dSGhpuzNYXX3yBU6saGhquWLGi\nvr7ex8fHzMxMV1f3jTfeiI+PxyPGysvL7e3tDQwMfH19Y2NjcT/zyZMnl5SUHDhwAGe6tra2PnHi\nRG5uLi7QzMwsJydHIpFcunTJ2Nh43759A2s/d+6c9Oxd/Zw7dw7v9sMPP4SFhTk5Oenr69PpdFdX\n14SEhJ6eHumiOjs7FyxYYG5ujhDS0dFxdnZOTk4eWJGFhcWWLVvwyp07d/7000/437t27cJz6uro\n6Li7u5eUlCh/bccGJcdZAqrGwO+KkIzy6Y8Jgjh16tSYfwenEePk2sL7TTUbA7+rcfecDgAASoK4\nCQA1V65ciY2Nxf8WCoUpKSnOzs50Ot3U1NTT07O+vn7gIT09PVOnTt21a5c85YvF4tDQUDabzWAw\nbG1tg4ODKysr5W+eWCw+fPjwoElqSktL58+fz2QybWxsYmJient7yU1JSUnu7u4mJib6+vrOzs47\nd+4cKq9ov3M5f/78Z599NkqTqCoM4iYAFOzZsyc9PT0uLg4vhoWFfffddydOnODxeL/++quTk9Og\n4SY+Pl46XYBsYrG4pKTk5MmTbW1tpaWlfD5/wYIFjY2N8hxbU1OzYMGCbdu2DezBVl1dvXjx4oCA\ngJaWlnPnzv3jH/8gZxNCCF27dm3Lli319fWtra0pKSlpaWlDDQTody5BQUEMBiMgIKC9vV3OExwL\nNP2CVVlo9L9j1lojfW15PB6Hw9F4UfJ/F9q/f7+Liwufz8eLOTk5BEFUVlbKPurmzZs4owqewHJY\nQqFw2bJl5OKdO3cQQtLfAIdSUVGxYsWK48ePe3t7T58+vd/WsLAwBwcHsViMFw8ePEgQxK+//ooX\nAwMDpVNq4pePDQ0Ncp4Ll8vlcDhCoVCeExwDf7Nwvwk0RoW5+9SQBrC2tnb37t2JiYkMBgOvOXr0\n6MyZM2VP6Mjn83fs2JGWliZ/RTQajUwhiBDC3cjq6uqGPXD69Ol5eXnvvPMOObEdSSQSFRYWLly4\nkOxz/vbbb0skEnJOt4sXL0qn1MRJG/rdtMo4l4SEhIqKCkqnOapB3ARKkUgkqampbm5u+vr6ZmZm\nISEhZLoTLpdLp9Nx5yeE0ObNmw0NDQmCaG1tRQj1y92Xnp7OYDCsrKw2btxoY2PDYDB8fHxu376t\nQFFIFZn9BkpPT5dIJEFBQXhRIBCUlZUNO11lfHz85s2bLS0tFa4XjwM2MTFRuASE0OPHj1+/fs1m\ns8k1uNfaUG9OX7x4YWBg4ODgIL1SxrmYmZktXLgwLS1NMsr758gJ4iZQSkJCQmxsbHx8fHNz840b\nN549e+bn59fU1IQQSk9Pl+5rkpGRkZiYSC72y93H5XIjIiJ4PF5UVFR9fX15eblIJFq0aBGe8pdS\nUUgVmf0GKiwsdHV1JRNINzY2CgSCe/fu+fv740Dv5uaWkZEhHThu3rxZV1enZCYq/Jzu6+urTCF/\n/PEHQsjY2Jhcw2AwDAwM8H+pfng83rVr1z766COczgYb9lxmzJjx4sULMrni2AZxEyiOz+enpqau\nWLFi/fr1LBbLy8vrq6++am1t/frrrxUrkEaj4VtXd3f3zMzMrq6u7OxsBcpRPrNfP93d3U+ePJEe\nwoC//1haWiYnJ1dXVzc1NYWEhGzZsuXkyZN4Bz6fHx0dnZmZqXClTU1Nubm5UVFRHA6HvM9VDP50\n3m9yCz09vUGT2qSkpNjY2JDTAiL5zmXKlCkIoaqqKmXaOVpA3ASKq66ufv369ezZs8k1c+bModPp\n5PO1MmbPns1kMsmnfs1qbm6WSCTSs5Xgd4geHh4+Pj7m5uYsFisxMZHFYpH/z4iLi/v444+VmdOJ\nw+FERUWFhIQUFRUpOXMkfidLJkLEBAIBOYcV6dy5c6dPn758+bL0zak854IvzqA3sGPPeMyHBFQF\ndz0xMjKSXmlqatrV1aWS8vX19VtaWlRSlJJ6enrQn7ESs7GxQQjhF6wYnU63t7fHH3BKS0urqqpS\nU1OVqdTKyiorK8vDw0OZQjD8arizs5Ncw+Pxenp68FmQcnNzU1NTi4uL33jjDXKlnOeCQzC+UGMe\n3G8CxZmamiKE+kXJ9vZ2Ozs75QsXCoWqKkp5OChI9+42MjKaMmVKvyxWIpEIpzLIysq6evWqjo4O\nntEef0tJTk4mCOLnn3+Ws1JLS0t8hZXn4OBgbGz89OlTcg1+ESydOObIkSPHjx+/du2adNBEcp8L\nzoo98AZ2TIK4CRTn6elpZGQk/cdz+/ZtgUBATk1Mo9EUzopfXFwskUhwtioli1KelZUVQRAdHR3S\nK8PCwu7fv//48WO8yOPxnj59irslZWdnS3f3w3fNuM+j9GsN2S5cuKCqqZtpNNrSpUtv3LhBfisr\nKioiCAK/NpVIJDExMVVVVfn5+f2eHuQ/F3xxcMqbMQ/iJlAcg8HYvn37uXPnjh8/3tnZWVVVFRkZ\naWNjs2HDBryDs7NzW1tbfn6+UChsaWmRvt9Bg+XuE4vFr169EolElZWV0dHRbDY7IiJCgaKoZvYb\nFpPJdHR0fP78ufTKbdu22dvbR0RENDQ0vHz5MiYmhs/nf/LJJ/IUGB4ebm1tXV5ePtQOtbW11tbW\nYWFhlI6SYffu3U1NTXv27Onu7r5169bBgwcjIiJcXV0RQg8fPvz888+PHTump6dHSDl06JD85eOL\nI7s365gBcRMoZc+ePSkpKUlJSRYWFgsXLpw8ebJ0XtFNmzb5+/uvXbvW1dV17969+CGOw+Hg3kWR\nkZFWVlbu7u5Lly7F83f29PR4eXkZGBj4+fm5uLhcv36dfKVItSiVCwwMrK6ulv4AbWZmVlJSYmdn\n5+3tbWtre+fOncLCwmF7dGICgaC5uZnsdj7QoB0hZR9VVlbm6+v7xhtv3L59+8GDBzY2NvPnz79x\n4wbe6uHhcfny5R9++GHChAkrV67829/+dvToURl1UXX37l1bW1tlMsaOJiM7HGnkodE/Zktrqfna\nbtiwwdzcXG3VkeQcZ1lTU0Oj0b7//nuVVNrX1+fn55eVlaWGo9SgtbWVwWAcOnRInp3HwN8s3G8C\nLaLNaXWcnZ2TkpKSkpKGShQkv76+vvz8/K6urvDw8JE+Sj0SEhK8vb25XK6mG6ImEDcBkFdsbOzq\n1avDw8P7fSCiqri4OC8vr6ioSLpD6AgdpQapqakVFRWXLl1SspPpKAJxE2iFuLi47Ozsjo4OBweH\ns2fParo5Q0pOTuZyufv371emkICAgBMnTpDD7Uf0qJFWUFDQ29tbXFys8JypoxH0ewdaISUlJSUl\nRdOtkMvixYtxLjWAEAoODg4ODtZ0K9QN7jcBAIAaiJsAAEANxE0AAKAG4iYAAFADcRMAAKghJKM8\nrz05XwoAYLQ4deqUdAL/UWfU90M6deqUppsARkRYWFh0dDSHw9F0Q4DqDTq9+ygy6u83wVhFEMRo\nvysBYxW83wQAAGogbgIAADUQNwEAgBqImwAAQA3ETQAAoAbiJgAAUANxEwAAqIG4CQAA1EDcBAAA\naiBuAgAANRA3AQCAGoibAABADcRNAACgBuImAABQA3ETAACogbgJAADUQNwEAABqIG4CAAA1EDcB\nAIAaiJsAAEANxE0AAKAG4iYAAFADcRMAAKiBuAkAANRA3AQAAGogbgIAADUQNwEAgBqImwAAQA3E\nTQAAoAbiJgAAUANxEwAAqIG4CQAA1NA03QAA/kdOTk5XV5f0mitXrrS3t5OLoaGhlpaWam8XAP0R\nEolE020AACGEIiIivv32Wz09PbyIf5kEQSCE+vr6jIyMmpub9fX1NdlEABBC8JwOtMfatWsRQsI/\niUQikUiE/62rq7t69WoImkBLwP0m0BYikcja2rqtrW3QrVevXn3zzTfV3CQABgX3m0Bb0Gi0tWvX\nks/p0iwsLBYuXKj+JgEwKIibQIusXbtWKBT2W6mnp/fuu+/q6upqpEkADATP6UCLSCQSNpv9/Pnz\nfuvv3LkzZ84cjTQJgIHgfhNoEYIg1q9f3+9RfdKkSbNnz9ZUkwAYCOIm0C79HtX19PQiIiJwbyQA\ntAQ8pwOtM3Xq1EePHpGLv/zyi4eHhwbbA0A/cL8JtM67775LPqq7u7tD0ATaBuIm0Drr168XiUQI\nIT09vffff1/TzQGgP3hOB9po9uzZ9+7dIwiivr6ezWZrujkA/B9wvwm00XvvvYcQmjt3LgRNoIVG\nZT6kW7dupaamaroVYAT19PQQBNHb27t69WpNtwWMIA6Hs23bNk23grJReb/57Nmzs2fParoV40hZ\nWVlZWZk6a2QwGNbW1nZ2duqs9Pnz5/C7UqeysrJbt25puhWKGJX3m9iZM2c03YTxAt/0qfmC19bW\nOjs7q7PG06dPh4WFwe9KbUbvw8SovN8E44GagyYA8oO4CQAA1EDcBAAAaiBuAgAANRA3AQCAGoib\nYKRcunSJxWJduHBB0w0ZKVeuXImNjcX/FgqFKSkpzs7OdDrd1NTU09Ozvr5+4CE9PT1Tp07dtWuX\nPOWLxeLQ0FA2m81gMGxtbYODgysrK+VvnlgsPnz4sI+Pz8BNpaWl8+fPZzKZNjb/v71zD2rq+B74\nXkzz4hGCEkQQ5CUKgmhtSxBEh5FWaYX6Aq0zpY4v1AlUaxEiCqlBrRYzVLBTm6ZjfSA+BlREO1VR\n8V0VofgEBFQqAUFeCSQh9/fHjvnmhxJuAglJ2M9f7t7ds2cz18Pe3bPnOCYmJnZ1dake8Xg8Hx8f\nGxsbCoXi6en5/ffft7e3v1d+j7mcPHlyx44d3d3dxDU0XZDdROgL877Cu2XLlszMzOTkZFiMjo7e\nv3//wYMHJRLJw4cPPTw83mtuuFyueqgnzSiVyitXrhw6dKipqam4uFgqlU6bNq2uro5I36dPn06b\nNm3dunUSiaTHo/Ly8vDw8LCwsIaGhhMnTvz+++9xcXGqpxcuXFi7dm11dXVjY2N6erpAIOjNW6jH\nXObMmUOlUsPCwtRTN5stuAly5MgRE9XcRJk/f/78+fMHW4tekUgkbDa7/3KIv1fbtm0bO3asVCqF\nxcOHD2MYVlpaqrnX1atXw8PDAQBcLpfIKHK5/PPPP1cVb926BQDg8/l9diwpKZk7d+6BAwcCAgIm\nTpzY42l0dLSbm5tSqYTFnTt3Yhj28OFDWIyIiFAoFKrGCxcuBADU1tYSnAuHw2Gz2XK5nMgEjfy9\n0gBabyJMHqFQKBaLDTZcRUVFSkpKWloalUqFNXv37p08ebKfn5+GXlKpdMOGDQKBgPhAJBJJfZfD\n3d0dAFBZWdlnx4kTJx4/fvyrr756N3OyQqEoKCgIDQ1VhYKeNWsWjuP5+fmwePr0afVUTiNGjAAA\n9Fi0aphLampqSUmJVtM0RZDdROiF4uJiFxcXDMP27NkDAMjOzra0tKTT6fn5+bNmzbKxsXF2dj58\n+DBsnJmZSaVSWSzWqlWrHB0dqVRqUFDQzZs34VMOh0Mmk0eOHAmLa9assbS0xDCssbERAJCQkLB+\n/frKykoMw6Cr/NmzZ21sbPh8vp6mlpmZieP4nDlzYFEmk924cSMgIEBzLy6Xu2bNGnt7e53HlUql\nAAAbGxudJQAAqqqq2tvb1aOleHh4AAB62zl9+fIljUZzc3NTr9QwFyaTGRoaKhAIcLPepUF2E6EX\ngoODr127piquXr3622+/lUql1tbWR44cqaysdHd3X758OUyJweFwYmNjJRJJfHx8dXX13bt3FQrF\nzJkznz9/DgDIzMyEX4uQrKystLQ0VVEgEHzxxRceHh44jldUVAAA4NGEUqnU09QKCgq8vb3pdDos\n1tXVyWSyO3fuzJgxAxr98ePHZ2VlqRuOq1evVlZWLl68uD/jwu/04ODg/gh59eoVAMDa2lpVQ6VS\naTRafX39u40lEsmFCxeWL19OJpNVlX3OZdKkSS9fvrx//35/9DRykN1EGJSgoCAbGxt7e/uYmJiO\njo7a2lrVIxKJNH78eAqF4uPjk52d3dbWJhKJdBgiIiKitbU1JSVl4LT+Hx0dHc+ePYNrNAg8/7G3\nt+fz+eXl5fX19VFRUWvXrj106BBsIJVKExISsrOzdR60vr4+JycnPj6ezWar1rm6AY/OeyRV/uCD\nD+Bitgfp6emOjo5bt25V1RCZi5eXFwCgrKysP3oaOchuIgYHuIR5N1s6ZMqUKXQ6/dGjR4ZVqm/E\nYjGO46rFJgAA7iH6+voGBQXZ2dkxGIy0tDQGg/Hrr7/CBsnJyStWrHByctJ5UDabHR8fHxUVVVhY\n2CPZp7bAPVkYTl+FTCaj0Wg9Wp44cSI3N/fcuXPqi1Mic4E/znsXsGaDCcdDQpg3FAqloaFhsLXo\nSWdnJ3hrKyGOjo4AALjZCiGTya6urvAAp7i4uKysrJ/hYlksllAoHJA8S3CbuLW1VVUjkUg6Ozvh\nLFTk5ORkZGQUFRWNGjVKVUlwLtAEwx/KXEHrTYQxIpfL37x5Y+D4m0SARkHdu9vKysrLy+vBgwfq\nzRQKBYPBAAAIhcLz589bWFhgGIZhGDxL4fP5GIb9888/BAe1t7e3tbUdEP3d3Nysra1rampUNXBT\n2N/fX1Xz888/Hzhw4MKFC+pGExCei0wmA29/KHMF2U2EMVJUVITjeGBgICySSKTevugNDIvFwjCs\npaVFvTI6OvrevXtVVVWwKJFIampqoFuSSCRS9/uDK2jo8zhlyhSCg546dao/n/nqkEik2bNnX758\nWXVuVlhYiGEY3DbFcTwxMbGsrCwvL8/KyqpHX4JzgT+Og4PDgChsnCC7iTAWlEplc3OzQqEoLS1N\nSEhwcXGJjY2Fjzw9PZuamvLy8uRyeUNDg/pyCQBgZ2dXV1dXXV3d1tYml8sLCwv154dEp9Pd3d1f\nvHihXrlu3TpXV9fY2Nja2trXr18nJiZKpdKNGzcSERgTE+Pg4HD37t3eGlRUVDg4OERHR2vVSwMp\nKSn19fVbtmzp6Oi4fv36zp07Y2Njvb29AQAPHjz48ccf9+3b98EHH2Bq7Nq1i7h8+ONo9mY1dZDd\nROiFPXv2fPTRRwCAxMTEyMjI7Ozs3bt3AwD8/f2rqqr27du3fv16AMBnn3329OlT2KWzs9PPz49G\no4WEhIwdO/bixYuqbcTVq1fPmDFj0aJF3t7eP/zwA/wGZLPZ0FEpLi6OxWL5+PjMnj27qalJ31OL\niIgoLy9XP4BmMplXrlxxdnYOCAhwcnK6detWQUFBnx6dEJlMJhaLVW7n7/JeR0jNvW7cuBEcHDxq\n1KibN2/ev3/f0dFx6tSply9fhk99fX3PnTv3119/DR8+fN68eUuXLt27d6+GsbTl9u3bTk5O6h/+\nZoi+LyTpA3TP0sAY4D7cypUr7ezs9DpEnxB8r54+fUoikf78888BGbS7uzskJEQoFBqglwFobGyk\nUqm7du0i0hjds0Qg+ouphNLx9PTk8Xg8Hq+3QEHE6e7uzsvLa2tri4mJ0Xcvw5CamhoQEMDhcAZb\nEf2C7CYCoTVJSUkLFiyIiYnpcUCkLUVFRcePHy8sLFR3CNVTLwOQkZFRUlJy5syZfjqZGj9DxW4u\nW7bM2toaw7CSkpLB1gUAAI4fP+7u7q6+9U4mk1ks1vTp03fu3Nnc3DzYChqU5ORkkUjU0tLi5uZm\nKpl4+Xw+h8PZtm1bf4SEhYUdPHhQdfVer730TX5+fldXV1FREZPJHGxd9M5QsZu//fbbvn37BluL\n/zFv3ryqqioPDw8Gg4HjuFKpFIvFubm5bm5uiYmJvr6+xJ37zID09PSuri4cx589ezZ//vzBVoco\n4eHh27dvH2wtjIXIyMikpKQeNzjNlaFiN40cDMNsbW2nT58uEolyc3Pr6+sjIiL6+Q2IQCD0xBCy\nm6qAg0bO/PnzY2NjxWLxL7/8Mti6IBCI92DOdhPH8Z07d3p7e1MoFAaDsWHDBvWn3d3dmzdvdnFx\nodFo/v7+0AdFc5hIAMClS5c+/vhjOp1uY2Pj5+cH7/m+VxToRyBI6O9dWFhoMFURCIQWDLIflE4Q\n9LPjcrkYhv3000/Nzc0SiSQrKwsAcO/ePfj0u+++o1Aox44da25uTk5OtrCwuH37NuwFADh//nxL\nS4tYLA4JCbG0tJTJZDiOt7e329jY7NixQyqVvnr1au7cuQ0NDRpEnT592tramsfj9aahan+zB9DG\njR492mCqasZ0/ey0AvkFGxjTfa9M8i0h8n5LJBI6nT5z5kxVDVyLQbsplUrpdHpMTIyqMYVCWb16\nNf7WGKlSx0BrW1FRgeP4v//+CwA4ffq0+kAaRPVJb3YTx3G442kkqpru+60VyG4aGNN9r8w2jlxF\nRYVEIgkLC3vv08ePH0skkgkTJsAijUYbOXLke6M9qoeJdHd3Z7FYS5YsiY+Pj42NHTNmjFaiiNPR\n0YHjOMyIYCSqHjt2zFQ2iPvJEJmmkWBC7hPqmK3dhMEFekvn0tHRAQDYtGmTeibrHiEI34VGo124\ncGHjxo18Pp/H4y1cuFAkEukmSjNPnjwBAIwbN854VA0MDPz222+1n4opcf36dYFAgPZ8DQYMWWCK\nmK3dhHGtYVaAd4H2dPfu3QkJCVqJ9fX1PXXqVENDQ0ZGxvbt2319feFdNx1EaeDs2bMAgFmzZhmP\nqs7OzupJfswVgUAwFKZpJBw9enSwVdARsz1PnzBhgoWFxaVLl977dPTo0VQqVdu7Q3V1dTA8rb29\n/bZt2yZPnvzgwQPdRGng1atXu3fvdnZ2Xrp0qZGrikAMTczWbtrb28+bN+/YsWNCobC1tbW0tFSV\n7wUAQKVSv/nmm8OHD2dnZ7e2tnZ3d7948eK///7TLLOurm7VqlWPHj2SyWT37t2rqakJDAzUIIpI\nIEgcx9vb25VKJY7jDQ0NR44cmTp16rBhw/Ly8uD+pmFURSAQWjDI51I6QfDcs62tbdmyZcOHD7ey\nsgoODt68eTMAwNnZ+f79+ziOd3V1JSYmuri4kEgkaGTLy8uzsrJgrAQvL6/Kyspff/0VGi9XV9cn\nT55UV1cHBQUxmcxhw4aNGjWKy+UqFIreROE4fubMGWtr661bt76r28mTJ/39/el0OplMtrCwAG+v\nDH388cc8Hu/169fqjQ2gqmZM99xTK9B5uoEx3fcKw00wPXxubm50dLQpam6iLFiwAJjybhRB0Htl\nYEz3vTLb73QEAoHQE8huIhB64e+//05KSoL/lsvl6enpnp6eZDLZ1tZ2woQJ1dXV73bp7OwcN26c\nupeYBpRK5Zdffuni4kKlUp2cnCIjI0tLSwnqpkGfrVu3Yv8flcPvyZMnd+zYYSrhpfUKspsIxMCz\nZcuWzMzM5ORkWIyOjt6/f//BgwclEsnDhw89PDzeGyuey+U+fvyY4BBKpfLKlSuHDh1qamoqLi6W\nSqXTpk2rq6sj0pegPj2YM2cOlUoNCwt78+YNQSXNFWQ3EUaBVCoNCgoyNlG6sX379pycnNzcXGtr\nawBATk5OXl7e0aNHP/nkExKJ5OjomJ+fr1rEqbh27Rq8HUscNpsdHBxMp9Pd3Nz4fH5LS8sff/zR\nZ68+9emROkldq/j4+IkTJ86ePVuhUGilqpmB7CbCKBAKhWKx2NhE6UBFRUVKSkpaWhq8eQEA2Lt3\n7+TJkzXnxZVKpRs2bBAIBMQHIpFIp06dUhXd3d0BAJWVlX12JKKPBlJTU0tKSrRS1fxAdhMxYOA4\nnpGRMX78eAqFwmQyo6KiVJffORwOmUxWpXZYs2aNpaUlhmGNjY0AgISEhPXr11dWVmIY5unpmZmZ\nSaVSWSzWqlWrHB0dqVRqUFDQzZs3dRAF+hHNTzcyMzNxHJ8zZw4symSyGzdu9JkTmMvlrlmzprdr\nwUSAeYmhL5oGCOqjASaTGRoaKhAIhrLjAbKbiAEjNTU1KSmJy+WKxeLLly8/f/48JCSkvr4eAJCZ\nmal+fzErKystLU1VFAgEX3zxhYeHB47jFRUVHA4nNjZWIpHEx8dXV1ffvXtXoVDMnDkTZkvXShR4\nmyZTqVTq/wcAAICCggJvb29VxrS6ujqZTHbnzp0ZM2bAvwHjx4/PyspSNzpXr16trKxcvHhxf8a9\ndesWACA4OFhzMyL6JCUlMZlMMpns5uYWFRV1+/btHkImTZr08uXL+/fv90dhkwbZTcTAIJVKMzIy\n5s6du2TJEgaD4efn98svvzQ2Nqpf09IKEokEl64+Pj7Z2dltbW0ikUgHOREREa2trSkpKbqpoRUd\nHR3Pnj3z8PBQ1cDzFnt7ez6fX15eXl9fHxUVtXbt2kOHDsEGUqk0ISEhOztb50Hr6+tzcnLi4+PZ\nbLZqndsbferz9ddfnzx58vnz5+3t7YcPH66trQ0NDS0vL1cX4uXlBQAoKyvTWWdTB9lNxMBQXl7e\n3t4+ZcoUVc1HH31EJpNV39f9YcqUKXQ6vZ/R+QyAWCzGcVw9PS+FQgEA+Pr6BgUF2dnZMRiMtLQ0\nBoOh+nOSnJy8YsUKJycnnQdls9nx8fFRUVGFhYV9JuDtU5/Ro0dPmjTJysqKTCYHBgaKRCKpVApD\nu6qAE4RfEkMTs42HhDAw0DfFyspKvdLW1ratrW1A5FMolIaGhgERpT86OzvBW9sEgWH64N4rhEwm\nu7q6wgOc4uLisrKyjIyM/gzKYrGEQqGvry+Rxpr1eRc/P79hw4bBwIYqaDQaeDvZoQlabyIGBltb\nWwBADyv55s0bZ2fn/guXy+UDJUqvQIOi7hluZWXl5eUFY1N8YA+sAAADvklEQVSpUCgUDAYDACAU\nCs+fP29hYQE9zOG5EJ/PxzCMeCJoe3t7+OMTQbM+76JUKpVKpfpfAgCATCYDbyc7NEF2EzEwTJgw\nwcrKSv1/+82bN2Uy2YcffgiLJBIJxqLXgaKiIhzHAwMD+y9Kr7BYLAzDeiRwjo6OvnfvXlVVFSxK\nJJKamhroBiQSidQ9JeGCmsvl4jiuvuOhmVOnTmn1ma9BHwDAp59+qt4Ypp9is9nqlXCCDg4OxAc1\nM5DdRAwMVCp1/fr1J06cOHDgQGtra1lZWVxcnKOj48qVK2EDT0/PpqamvLw8uVze0NBQU1Oj3t3O\nzq6urq66urqtrQ3aRKVS2dzcrFAoSktLExISXFxcYJpPbUURieY3UNDpdHd3d5hrQMW6detcXV1j\nY2Nra2tfv36dmJgolUo3btxIRGBMTIyDg8Pdu3d7a1BRUeHg4BAdHU28l2Z9Xr58mZOT8+bNG7lc\nfv369WXLlrm4uMTFxalLgBPU2QPUDEB2EzFgbNmyJT09ncfjjRgxIjQ0dMyYMUVFRZaWlvDp6tWr\nZ8yYsWjRIm9v7x9++AF+5bHZbOhdFBcXx2KxfHx8Zs+e3dTUBADo7Oz08/Oj0WghISFjx469ePGi\n6mtRW1GGJCIiory8HHpTQphM5pUrV5ydnQMCApycnG7dulVQUEDQg1Imk4nF4vz8/N4avNeJUnMv\nzfp89tlnmzZtcnZ2ptPpCxcunDp16o0bN4YPH64u4fbt205OTv7+/kSmYJ7oN0ydfkBxEg2M4eMk\nrly50s7OzpAj4gP0Xj19+pREIvW4qqgz3d3dISEhQqHQAL0I0tjYSKVSd+3a1X9Rpht/E603EUaK\nicbd8fT05PF4PB6PSKQMzXR3d+fl5bW1tcHEUHrtRZzU1NSAgAAOh6MP4aYCspsIxACTlJS0YMGC\nmJiYHgdE2lJUVHT8+PHCwkJ1h1A99SJIRkZGSUnJmTNn+nQUNW+Q3UQYHcnJySKRqKWlxc3N7dix\nY4Otji7w+XwOh7Nt27b+CAkLCzt48KDqJr5eexEhPz+/q6urqKiIyWQOuHDTAvm9I4yO9PT09PT0\nwdaiv4SHh4eHhw+2FgNJZGRkZGTkYGthFKD1JgKBQGgHspsIBAKhHchuIhAIhHYgu4lAIBDaYcLn\nQrm5uYOtwlAB3qsz+x/8+vXrYAhM03h48eKF8cdqeT+D7XivC/BeBwKBMHVM9L4Qhg/hJCEIBAKh\nA2h/E4FAILQD2U0EAoHQDmQ3EQgEQjuQ3UQgEAjt+D+k2lIo1gYIxwAAAABJRU5ErkJggg==\n",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 28,
     "metadata": {
      "tags": []
     },
     "output_type": "execute_result"
    }
   ],
   "source": [
    "tf.keras.utils.plot_model(\n",
    "    model,\n",
    "    show_shapes=True,\n",
    "    show_layer_names=True,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "JpL9idwZV6fL"
   },
   "source": [
    "For each character the model looks up the embedding, runs the GRU one timestep with the embedding as input, and applies the dense layer to generate logits predicting the log-likelihood of the next character:\n",
    "\n",
    "![Model architecture](https://www.tensorflow.org/tutorials/text/images/text_generation_training.png)\n",
    "\n",
    "Image source: [Text generation with an RNN](https://www.tensorflow.org/tutorials/text/text_generation) notebook."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "Npruiy2RAPkt"
   },
   "source": [
    "## Try the model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 34
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 11144,
     "status": "ok",
     "timestamp": 1584519814917,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "E4DCLA0GASL1",
    "outputId": "eba67358-d6b9-4b42-a2f1-dc6ed3e0708c"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(64, 100, 65) # (batch_size, sequence_length, vocab_size)\n"
     ]
    }
   ],
   "source": [
    "for input_example_batch, target_example_batch in dataset.take(1):\n",
    "    example_batch_predictions = model(input_example_batch)\n",
    "    print(example_batch_predictions.shape, \"# (batch_size, sequence_length, vocab_size)\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "MWebJXU9CEPd"
   },
   "source": [
    "To get actual predictions from the model we need to sample from the output distribution, to get actual character indices. This distribution is defined by the logits over the character vocabulary."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 340
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 11020,
     "status": "ok",
     "timestamp": 1584519814919,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "Y4Jgo-iECFWI",
    "outputId": "a59cb406-3813-4fb0-8297-6862a73f2499"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Prediction for the 1st letter of the batch 1st sequense:\n",
      "tf.Tensor(\n",
      "[-5.5658985e-03 -5.6167855e-03  2.3333444e-03 -5.4010577e-03\n",
      " -1.2658490e-03 -2.0685978e-03 -1.7119508e-03 -1.9059415e-03\n",
      " -1.8099162e-03  1.8549486e-03 -6.1538210e-03  2.1649192e-03\n",
      "  5.9574037e-03  3.1515316e-03 -3.3867492e-03  6.0820174e-03\n",
      "  2.0375988e-03  1.1445739e-03  1.3204901e-03 -2.5559508e-03\n",
      " -6.7244383e-04  2.0627938e-03 -5.1490301e-03  5.0887186e-03\n",
      " -2.1955089e-03 -5.3793588e-03  3.5228319e-03 -1.3459967e-03\n",
      "  1.5698730e-03 -3.6733383e-03 -2.6844436e-04  1.2893401e-03\n",
      " -2.9499468e-03 -2.0524827e-03  4.9254433e-03 -1.4868007e-03\n",
      " -3.2468680e-03 -3.1228375e-03  1.4175450e-03  2.6028175e-03\n",
      " -1.9702244e-04 -1.5901095e-04 -5.2650859e-05 -2.0922027e-03\n",
      " -1.1370353e-03 -1.3396480e-03  2.7639134e-04  2.6140050e-03\n",
      "  3.0784088e-04 -7.5585191e-04  2.4966525e-03  1.0233605e-03\n",
      "  1.4258339e-03 -1.6010429e-04  5.1720534e-03  9.4948718e-03\n",
      " -4.6309843e-03  5.5265515e-03 -1.2462666e-04 -1.3864412e-03\n",
      " -2.6601439e-03  2.0305226e-03  1.0555825e-03  1.6011465e-03\n",
      "  2.9543908e-03], shape=(65,), dtype=float32)\n"
     ]
    }
   ],
   "source": [
    "print('Prediction for the 1st letter of the batch 1st sequense:')\n",
    "print(example_batch_predictions[0, 0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 34
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 10898,
     "status": "ok",
     "timestamp": 1584519814922,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "0dOr0MwFHlRb",
    "outputId": "f8dc2264-a19a-4887-f964-9086a95b14a4"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tf.Tensor([[2 2 2 1 2]], shape=(1, 5), dtype=int64)\n"
     ]
    }
   ],
   "source": [
    "# Quick overview of how tf.random.categorical() works.\n",
    "\n",
    "# logits is 2-D Tensor with shape [batch_size, num_classes].\n",
    "# Each slice [i, :] represents the unnormalized log-probabilities for all classes.\n",
    "# In the example below we say that the probability for class \"0\" is low but the\n",
    "# probability for class \"2\" is much higher.\n",
    "tmp_logits = [\n",
    "  [-0.95, 0, 0.95],\n",
    "];\n",
    "\n",
    "# Let's generate 5 samples. Each sample is a class index. Class probabilities \n",
    "# are being taken into account (we expect to see more samples of class \"2\").\n",
    "tmp_samples = tf.random.categorical(\n",
    "    logits=tmp_logits,\n",
    "    num_samples=5\n",
    ")\n",
    "\n",
    "print(tmp_samples)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 34
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 10502,
     "status": "ok",
     "timestamp": 1584519814922,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "JPzr0r4zCgS3",
    "outputId": "23e7516b-1753-402a-bc61-8a8988e49f18"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "TensorShape([100, 1])"
      ]
     },
     "execution_count": 32,
     "metadata": {
      "tags": []
     },
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sampled_indices = tf.random.categorical(\n",
    "    logits=example_batch_predictions[0],\n",
    "    num_samples=1\n",
    ")\n",
    "\n",
    "sampled_indices.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 34
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 10276,
     "status": "ok",
     "timestamp": 1584519814923,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "YaA7DclID8dz",
    "outputId": "08a3c58b-e331-4f91-8e3f-be2649a85f48"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(100,)"
      ]
     },
     "execution_count": 33,
     "metadata": {
      "tags": []
     },
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sampled_indices = tf.squeeze(\n",
    "    input=sampled_indices,\n",
    "    axis=-1\n",
    ").numpy()\n",
    "\n",
    "sampled_indices.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 119
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 10212,
     "status": "ok",
     "timestamp": 1584519814924,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "_ubGQ0gVENhB",
    "outputId": "4ab910cf-06e6-4fd1-c751-746d79d895d0"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([62, 61, 40, 20,  4, 44, 17, 43, 28, 18, 50, 48,  0, 11, 58, 17, 41,\n",
       "       22, 62, 46, 12, 24, 25, 54, 10,  6, 18,  5,  4, 23, 55, 51, 38,  9,\n",
       "       50, 42, 42,  9, 49,  4, 48, 54, 59,  4, 30,  7, 20,  4, 53, 27, 21,\n",
       "        5, 16, 27, 25, 47, 21,  7, 24, 25, 46, 21,  6, 56,  1, 21, 24, 28,\n",
       "       55,  8, 23,  0, 45, 29,  4, 29,  5, 49, 25, 45,  3,  4, 22, 11, 13,\n",
       "       30, 43,  0, 62,  0, 48, 29, 33, 58, 46, 51, 12, 59, 55, 62])"
      ]
     },
     "execution_count": 34,
     "metadata": {
      "tags": []
     },
     "output_type": "execute_result"
    }
   ],
   "source": [
    "sampled_indices"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 122
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 9992,
     "status": "ok",
     "timestamp": 1584519814925,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "Gi9HOzw9EajS",
    "outputId": "aa7dc69f-63ad-4273-b2df-e6cc8eada7cd"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Input:\n",
      " 'is service.\\n\\nBAPTISTA:\\nA thousand thanks, Signior Gremio.\\nWelcome, good Cambio.\\nBut, gentle sir, met'\n",
      "\n",
      "Next char prediction:\n",
      " \"xwbH&fEePFlj\\n;tEcJxh?LMp:,F'&KqmZ3ldd3k&jpu&R-H&oOI'DOMiI-LMhI,r ILPq.K\\ngQ&Q'kMg$&J;ARe\\nx\\njQUthm?uqx\"\n"
     ]
    }
   ],
   "source": [
    "print('Input:\\n', repr(''.join(index2char[input_example_batch[0]])))\n",
    "print()\n",
    "print('Next char prediction:\\n', repr(''.join(index2char[sampled_indices])))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 272
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 9825,
     "status": "ok",
     "timestamp": 1584519814926,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "b87e0lsYMTsv",
    "outputId": "75c7e1bf-b2d5-4f15-cba7-0ae5f8765327"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Prediction  0\n",
      "  input: 47 ('i')\n",
      "  next predicted: 1 ('x')\n",
      "Prediction  1\n",
      "  input: 57 ('s')\n",
      "  next predicted: 1 ('w')\n",
      "Prediction  2\n",
      "  input: 1 (' ')\n",
      "  next predicted: 1 ('b')\n",
      "Prediction  3\n",
      "  input: 57 ('s')\n",
      "  next predicted: 1 ('H')\n",
      "Prediction  4\n",
      "  input: 43 ('e')\n",
      "  next predicted: 1 ('&')\n"
     ]
    }
   ],
   "source": [
    "for i, (input_idx, sample_idx) in enumerate(zip(input_example_batch[0][:5], sampled_indices[:5])):\n",
    "    print('Prediction {:2d}'.format(i))\n",
    "    print('  input: {} ({:s})'.format(input_idx, repr(index2char[input_idx])))\n",
    "    print('  next predicted: {} ({:s})'.format(target_idx, repr(index2char[sample_idx])))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "LqcBufKEE_p6"
   },
   "source": [
    "## Train the model\n",
    "\n",
    "At this point the problem can be treated as a standard classification problem. Given the previous RNN state, and the input this time step, predict the class of the next character."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "l4s0-PvrFub5"
   },
   "source": [
    "### Attach an optimizer, and a loss function"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 51
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 9644,
     "status": "ok",
     "timestamp": 1584519814927,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "UOEUUm6JE95a",
    "outputId": "ba94797c-e3c6-4d71-ac44-2cf76f7b2843"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Prediction shape:  (64, 100, 65)  # (batch_size, sequence_length, vocab_size)\n",
      "scalar_loss:       4.17424\n"
     ]
    }
   ],
   "source": [
    "# An objective function.\n",
    "# The function is any callable with the signature scalar_loss = fn(y_true, y_pred).\n",
    "def loss(labels, logits):\n",
    "    return tf.keras.losses.sparse_categorical_crossentropy(\n",
    "      y_true=labels,\n",
    "      y_pred=logits,\n",
    "      from_logits=True\n",
    "    )\n",
    "\n",
    "example_batch_loss = loss(target_example_batch, example_batch_predictions)\n",
    "\n",
    "print(\"Prediction shape: \", example_batch_predictions.shape, \" # (batch_size, sequence_length, vocab_size)\")\n",
    "print(\"scalar_loss:      \", example_batch_loss.numpy().mean())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "SXhJsB6eFgrJ"
   },
   "outputs": [],
   "source": [
    "adam_optimizer = tf.keras.optimizers.Adam(learning_rate=0.001)\n",
    "model.compile(\n",
    "    optimizer=adam_optimizer,\n",
    "    loss=loss\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "MK3Cf-xZFwL4"
   },
   "source": [
    "### Configure checkpoints"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "LUhXnHPJFy5q"
   },
   "outputs": [],
   "source": [
    "# Directory where the checkpoints will be saved.\n",
    "checkpoint_dir = 'tmp/checkpoints'\n",
    "os.makedirs(checkpoint_dir, exist_ok=True)\n",
    "\n",
    "# Name of the checkpoint files\n",
    "checkpoint_prefix = os.path.join(checkpoint_dir, 'ckpt_{epoch}')\n",
    "\n",
    "checkpoint_callback=tf.keras.callbacks.ModelCheckpoint(\n",
    "    filepath=checkpoint_prefix,\n",
    "    save_weights_only=True\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "oFg9MFJoGZWf"
   },
   "source": [
    "### Execute the training"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "AVk-pARPGaja"
   },
   "outputs": [],
   "source": [
    "EPOCHS=40"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 1000
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1197604,
     "status": "ok",
     "timestamp": 1584521003714,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "y0rveBdAGeEz",
    "outputId": "2449548c-5ca4-4340-df15-9cdf1714dcbc"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Train for 172 steps\n",
      "Epoch 1/40\n",
      "172/172 [==============================] - 31s 179ms/step - loss: 2.5607\n",
      "Epoch 2/40\n",
      "172/172 [==============================] - 29s 171ms/step - loss: 1.8654\n",
      "Epoch 3/40\n",
      "172/172 [==============================] - 30s 172ms/step - loss: 1.6191\n",
      "Epoch 4/40\n",
      "172/172 [==============================] - 30s 172ms/step - loss: 1.4893\n",
      "Epoch 5/40\n",
      "172/172 [==============================] - 30s 172ms/step - loss: 1.4108\n",
      "Epoch 6/40\n",
      "172/172 [==============================] - 30s 172ms/step - loss: 1.3550\n",
      "Epoch 7/40\n",
      "172/172 [==============================] - 30s 172ms/step - loss: 1.3101\n",
      "Epoch 8/40\n",
      "172/172 [==============================] - 30s 172ms/step - loss: 1.2705\n",
      "Epoch 9/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 1.2335\n",
      "Epoch 10/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 1.1980\n",
      "Epoch 11/40\n",
      "172/172 [==============================] - 29s 171ms/step - loss: 1.1610\n",
      "Epoch 12/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 1.1230\n",
      "Epoch 13/40\n",
      "172/172 [==============================] - 30s 174ms/step - loss: 1.0843\n",
      "Epoch 14/40\n",
      "172/172 [==============================] - 30s 174ms/step - loss: 1.0446\n",
      "Epoch 15/40\n",
      "172/172 [==============================] - 30s 172ms/step - loss: 1.0031\n",
      "Epoch 16/40\n",
      "172/172 [==============================] - 30s 174ms/step - loss: 0.9617\n",
      "Epoch 17/40\n",
      "172/172 [==============================] - 30s 172ms/step - loss: 0.9211\n",
      "Epoch 18/40\n",
      "172/172 [==============================] - 30s 172ms/step - loss: 0.8810\n",
      "Epoch 19/40\n",
      "172/172 [==============================] - 30s 172ms/step - loss: 0.8413\n",
      "Epoch 20/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 0.8018\n",
      "Epoch 21/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 0.7692\n",
      "Epoch 22/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 0.7346\n",
      "Epoch 23/40\n",
      "172/172 [==============================] - 30s 172ms/step - loss: 0.7056\n",
      "Epoch 24/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 0.6777\n",
      "Epoch 25/40\n",
      "172/172 [==============================] - 30s 174ms/step - loss: 0.6528\n",
      "Epoch 26/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 0.6282\n",
      "Epoch 27/40\n",
      "172/172 [==============================] - 30s 172ms/step - loss: 0.6077\n",
      "Epoch 28/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 0.5897\n",
      "Epoch 29/40\n",
      "172/172 [==============================] - 30s 172ms/step - loss: 0.5735\n",
      "Epoch 30/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 0.5566\n",
      "Epoch 31/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 0.5447\n",
      "Epoch 32/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 0.5318\n",
      "Epoch 33/40\n",
      "172/172 [==============================] - 30s 174ms/step - loss: 0.5198\n",
      "Epoch 34/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 0.5103\n",
      "Epoch 35/40\n",
      "172/172 [==============================] - 30s 172ms/step - loss: 0.4999\n",
      "Epoch 36/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 0.4930\n",
      "Epoch 37/40\n",
      "172/172 [==============================] - 30s 174ms/step - loss: 0.4858\n",
      "Epoch 38/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 0.4782\n",
      "Epoch 39/40\n",
      "172/172 [==============================] - 30s 173ms/step - loss: 0.4719\n",
      "Epoch 40/40\n",
      "172/172 [==============================] - 29s 170ms/step - loss: 0.4672\n"
     ]
    }
   ],
   "source": [
    "history = model.fit(\n",
    "  x=dataset,\n",
    "  epochs=EPOCHS,\n",
    "  callbacks=[\n",
    "    checkpoint_callback\n",
    "  ]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "mLdnOyvzMggJ"
   },
   "outputs": [],
   "source": [
    "def render_training_history(training_history):\n",
    "    loss = training_history.history['loss']\n",
    "    plt.title('Loss')\n",
    "    plt.xlabel('Epoch')\n",
    "    plt.ylabel('Loss')\n",
    "    plt.plot(loss, label='Training set')\n",
    "    plt.legend()\n",
    "    plt.grid(linestyle='--', linewidth=1, alpha=0.5)\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 295
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1197218,
     "status": "ok",
     "timestamp": 1584521003724,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "4Ghveem_OQBV",
    "outputId": "f4fcc0aa-4509-4330-a88b-1f5863c354d2"
   },
   "outputs": [
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "tags": []
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "render_training_history(history)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "X-dhNP2OG2EM"
   },
   "source": [
    "## Generate text"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "SU_YfP6sG3NC"
   },
   "source": [
    "### Restore the latest checkpoint\n",
    "\n",
    "To keep this prediction step simple, use a batch size of 1.\n",
    "\n",
    "Because of the way the RNN state is passed from timestep to timestep, the model only accepts a fixed batch size once built.\n",
    "\n",
    "To run the model with a different `batch_size`, we need to rebuild the model and restore the weights from the checkpoint."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 34
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1197040,
     "status": "ok",
     "timestamp": 1584521003726,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "BlG8o3wiG6f2",
    "outputId": "031b45e0-fcdf-4ab6-b7e9-e167b29800c9"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'tmp/checkpoints/ckpt_40'"
      ]
     },
     "execution_count": 44,
     "metadata": {
      "tags": []
     },
     "output_type": "execute_result"
    }
   ],
   "source": [
    "tf.train.latest_checkpoint(checkpoint_dir)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "l7evN0LvH01P"
   },
   "outputs": [],
   "source": [
    "simplified_batch_size = 1\n",
    "\n",
    "model = build_model(vocab_size, embedding_dim, rnn_units, batch_size=1)\n",
    "\n",
    "model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))\n",
    "\n",
    "model.build(tf.TensorShape([simplified_batch_size, None]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 255
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1196901,
     "status": "ok",
     "timestamp": 1584521004053,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "Y3eduDtZI9zQ",
    "outputId": "95ccdb2f-f059-4be2-ee85-3541f422c7ea"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Model: \"sequential_2\"\n",
      "_________________________________________________________________\n",
      "Layer (type)                 Output Shape              Param #   \n",
      "=================================================================\n",
      "embedding_2 (Embedding)      (1, None, 256)            16640     \n",
      "_________________________________________________________________\n",
      "lstm_1 (LSTM)                (1, None, 1024)           5246976   \n",
      "_________________________________________________________________\n",
      "dense_1 (Dense)              (1, None, 65)             66625     \n",
      "=================================================================\n",
      "Total params: 5,330,241\n",
      "Trainable params: 5,330,241\n",
      "Non-trainable params: 0\n",
      "_________________________________________________________________\n"
     ]
    }
   ],
   "source": [
    "model.summary()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "euNvAtr4JC3A"
   },
   "source": [
    "### The prediction loop\n",
    "\n",
    "The following code block generates the text:\n",
    "\n",
    "- It Starts by choosing a start string, initializing the RNN state and setting the number of characters to generate.\n",
    "\n",
    "- Get the prediction distribution of the next character using the start string and the RNN state.\n",
    "\n",
    "- Then, use a categorical distribution to calculate the index of the predicted character. Use this predicted character as our next input to the model.\n",
    "\n",
    "- The RNN state returned by the model is fed back into the model so that it now has more context, instead than only one character. After predicting the next character, the modified RNN states are again fed back into the model, which is how it learns as it gets more context from the previously predicted characters.\n",
    "\n",
    "![Prediction loop](https://www.tensorflow.org/tutorials/text/images/text_generation_sampling.png)\n",
    "\n",
    "Image source: [Text generation with an RNN](https://www.tensorflow.org/tutorials/text/text_generation) notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "bOqdqGouJFf_"
   },
   "outputs": [],
   "source": [
    "# num_generate\n",
    "# - number of characters to generate.\n",
    "#\n",
    "# temperature\n",
    "# - Low temperatures results in more predictable text.\n",
    "# - Higher temperatures results in more surprising text.\n",
    "# - Experiment to find the best setting.\n",
    "def generate_text(model, start_string, num_generate = 1000, temperature=1.0):\n",
    "    # Evaluation step (generating text using the learned model)\n",
    "\n",
    "    # Converting our start string to numbers (vectorizing).\n",
    "    input_indices = [char2index[s] for s in start_string]\n",
    "    input_indices = tf.expand_dims(input_indices, 0)\n",
    "\n",
    "    # Empty string to store our results.\n",
    "    text_generated = []\n",
    "\n",
    "    # Here batch size == 1.\n",
    "    model.reset_states()\n",
    "    for char_index in range(num_generate):\n",
    "        predictions = model(input_indices)\n",
    "        # remove the batch dimension\n",
    "        predictions = tf.squeeze(predictions, 0)\n",
    "\n",
    "        # Using a categorical distribution to predict the character returned by the model.\n",
    "        predictions = predictions / temperature\n",
    "        predicted_id = tf.random.categorical(\n",
    "        predictions,\n",
    "        num_samples=1\n",
    "        )[-1,0].numpy()\n",
    "\n",
    "        # We pass the predicted character as the next input to the model\n",
    "        # along with the previous hidden state.\n",
    "        input_indices = tf.expand_dims([predicted_id], 0)\n",
    "\n",
    "        text_generated.append(index2char[predicted_id])\n",
    "\n",
    "    return (start_string + ''.join(text_generated))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 578
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1202383,
     "status": "ok",
     "timestamp": 1584521010077,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "Z-8e8P60J5Pg",
    "outputId": "25240489-3063-4bdd-b203-ef14886d1e60"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ROMEO: God do not burden me.\n",
      "\n",
      "GREMIO:\n",
      "I want the king that murders writ upon your tongue\n",
      "And born too supplict, but thanks\n",
      "We should to him and nothing can I never my\n",
      "brothers forbeard our words:\n",
      "Confess which have wine, and provide thee\n",
      "And see how did bestride our nobising duke,\n",
      "Whose souls more pleased, that men are worst challenge into a farmet together\n",
      "Let hate not so, for these own with him.\n",
      "\n",
      "Tailors:\n",
      "But now the blood of two,\n",
      "You and your accused find your wars.\n",
      "\n",
      "MENENIUS:\n",
      "Part not chastisement; hidest me well arm'd\n",
      "With eterpity of the mon poor house.\n",
      "\n",
      "Third Servant:\n",
      "When you are gentlemen, some interr'd\n",
      "As is my goods, God says King Richard say,\n",
      "As is the sin that she doth aim at you\n",
      "As I could pick my daughter woful side!\n",
      "Thou follow'st me in his life, against your husband's land:\n",
      "My lawful king is dead! wis gone to ET:\n",
      "Where is my woful? Was never man so bath\n",
      "Lord Her further.\n",
      "\n",
      "BRUTUS:\n",
      "Let me prepare from fair, nor ambition\n",
      "That spucish him our absence, like a grave is no more drea\n"
     ]
    }
   ],
   "source": [
    "# Generate the text with default temperature (1.0).\n",
    "print(generate_text(model, start_string=u\"ROMEO: \"))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 561
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 1208506,
     "status": "ok",
     "timestamp": 1584521016280,
     "user": {
      "displayName": "Oleksii Trekhleb",
      "photoUrl": "https://lh3.googleusercontent.com/a-/AOh14GiiA4aUKCbFho88Jd0WWMoAqQUt3jbuCtfNYpHVOA=s64",
      "userId": "03172675069638383074"
     },
     "user_tz": -60
    },
    "id": "wq_NlwWJSdix",
    "outputId": "408ecde5-0646-4eda-902e-071e4f123417"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ROMEO: hexen his virginallike.\n",
      "\n",
      "BRUTUS:\n",
      "For thyself\n",
      "Look, sirr; let not this innocent mine friends\n",
      "And hateful dogs as appeared, for it exced\n",
      "Exelling clouds with gentle knterpleaking:\n",
      "Their liberal dogs not his;\n",
      "For Yorkome help, ho! ay, I trow:\n",
      "it will we meet us king E:\n",
      "Needs friends, father, in that mad bessisking in\n",
      "Whom I might thrive is rich'd die by thee?\n",
      "\n",
      "CORIOLANUS:\n",
      "Brak how, gnow roaring with Sicils\n",
      "Up Jusul's dee Signior Katharina,\n",
      "And thyself shalt hear of that which the ear o' Lord:\n",
      "You are the first gape on me at first,\n",
      "Thou lovest me wash:\n",
      "By heaven, I'll haughthere.\n",
      "O, gentle marvantio; and beginn pluck'd\n",
      "But the christs and Riciards a\n",
      "LUUMEOfoke fray,\n",
      "And yet whick, in my doesder differences be runed by the king,\n",
      "Who in his monthry glorious regeal death.\n",
      "O, that she smooth thy cause?\n",
      "Was e turn be so\n",
      "to thine as thee won that Edward, king, widdwisk\n",
      "Ere te goil: deserve not thy mother leave for reason\n",
      "many an hourable fought, as they do,\n",
      "If you should esear mon-judgment,\n",
      "Nor \n"
     ]
    }
   ],
   "source": [
    "# Generate the text with higher temperature to get more unexpected results.\n",
    "print(generate_text(model, start_string=u\"ROMEO: \", temperature=1.5))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "0hh80MqEO_XI"
   },
   "source": [
    "## Save the model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 0,
   "metadata": {
    "colab": {},
    "colab_type": "code",
    "id": "VPE98xa8PA-u"
   },
   "outputs": [],
   "source": [
    "model_name = 'text_generation_shakespeare_rnn.h5'\n",
    "model.save(model_name, save_format='h5')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "WYP08xbbTNKp"
   },
   "source": [
    "## Converting the model to web-format\n",
    "\n",
    "To use this model on the web we need to convert it into the format that will be understandable by [tensorflowjs](https://www.tensorflow.org/js). To do so we may use [tfjs-converter](https://github.com/tensorflow/tfjs/tree/master/tfjs-converter) as following:\n",
    "\n",
    "```\n",
    "tensorflowjs_converter --input_format keras \\\n",
    "  ./experiments/text_generation_shakespeare_rnn/text_generation_shakespeare_rnn.h5 \\\n",
    "  ./demos/public/models/text_generation_shakespeare_rnn\n",
    "```\n",
    "\n",
    "You find this experiment in the [Demo app](https://trekhleb.github.io/machine-learning-experiments) and play around with it right in you browser to see how the model performs in real life."
   ]
  }
 ],
 "metadata": {
  "accelerator": "GPU",
  "colab": {
   "collapsed_sections": [],
   "name": "text_generation_shakespeare_rnn.ipynb",
   "provenance": [],
   "toc_visible": true
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
